diff --git a/include/libcamera/control_ids.h.in b/include/libcamera/control_ids.h.in
index 0718a8886f6c..c97b09a82450 100644
--- a/include/libcamera/control_ids.h.in
+++ b/include/libcamera/control_ids.h.in
@@ -32,6 +32,8 @@ ${draft_controls}
 
 } /* namespace draft */
 
+${vendor_controls}
+
 } /* namespace controls */
 
 } /* namespace libcamera */
diff --git a/include/libcamera/meson.build b/include/libcamera/meson.build
index a24c50d66a82..2c8c0258c95e 100644
--- a/include/libcamera/meson.build
+++ b/include/libcamera/meson.build
@@ -32,20 +32,21 @@ install_headers(libcamera_public_headers,
 
 libcamera_headers_install_dir = get_option('includedir') / libcamera_include_dir
 
-# control_ids.h and property_ids.h
-control_source_files = [
-    'control_ids',
-    'property_ids',
-]
+# control_ids.h and property_ids.h and associated modes
+control_source_files = {
+    'control_ids': 'controls',
+    'property_ids': 'properties',
+}
 
 control_headers = []
 
-foreach header : control_source_files
+foreach header, mode : control_source_files
     input_files = files('../../src/libcamera/' + header +'.yaml', header + '.h.in')
     control_headers += custom_target(header + '_h',
                                      input : input_files,
                                      output : header + '.h',
-                                     command : [gen_controls, '-o', '@OUTPUT@', '@INPUT@'],
+                                     command : [gen_controls, '-o', '@OUTPUT@', '@INPUT@',
+                                                '--mode', mode],
                                      install : true,
                                      install_dir : libcamera_headers_install_dir)
 endforeach
diff --git a/include/libcamera/property_ids.h.in b/include/libcamera/property_ids.h.in
index ff0194083af0..47c5d6bf2e28 100644
--- a/include/libcamera/property_ids.h.in
+++ b/include/libcamera/property_ids.h.in
@@ -31,6 +31,8 @@ ${draft_controls}
 
 extern const ControlIdMap properties;
 
+${vendor_controls}
+
 } /* namespace properties */
 
 } /* namespace libcamera */
diff --git a/src/libcamera/control_ids.cpp.in b/src/libcamera/control_ids.cpp.in
index 5fb1c2c30558..bdb312759eb1 100644
--- a/src/libcamera/control_ids.cpp.in
+++ b/src/libcamera/control_ids.cpp.in
@@ -33,6 +33,8 @@ ${draft_controls_doc}
 
 } /* namespace draft */
 
+${vendor_controls_doc}
+
 #ifndef __DOXYGEN__
 /*
  * Keep the controls definitions hidden from doxygen as it incorrectly parses
@@ -45,6 +47,9 @@ namespace draft {
 ${draft_controls_def}
 
 } /* namespace draft */
+
+${vendor_controls_def}
+
 #endif
 
 /**
diff --git a/src/libcamera/control_ids.yaml b/src/libcamera/control_ids.yaml
index 5827d7ecef49..ff74ce1deedb 100644
--- a/src/libcamera/control_ids.yaml
+++ b/src/libcamera/control_ids.yaml
@@ -6,6 +6,7 @@
 ---
 # Unless otherwise stated, all controls are bi-directional, i.e. they can be
 # set through Request::controls() and returned out through Request::metadata().
+vendor: libcamera
 controls:
   - AeEnable:
       type: bool
diff --git a/src/libcamera/meson.build b/src/libcamera/meson.build
index d0e26f6b4141..e49bf850b355 100644
--- a/src/libcamera/meson.build
+++ b/src/libcamera/meson.build
@@ -127,12 +127,13 @@ endif
 
 control_sources = []
 
-foreach source : control_source_files
+foreach source, mode : control_source_files
     input_files = files(source +'.yaml', source + '.cpp.in')
     control_sources += custom_target(source + '_cpp',
                                      input : input_files,
                                      output : source + '.cpp',
-                                     command : [gen_controls, '-o', '@OUTPUT@', '@INPUT@'])
+                                     command : [gen_controls, '-o', '@OUTPUT@', '@INPUT@',
+                                                '--mode', mode])
 endforeach
 
 libcamera_sources += control_sources
diff --git a/src/libcamera/property_ids.cpp.in b/src/libcamera/property_ids.cpp.in
index f917e3349766..eed1124f3eb7 100644
--- a/src/libcamera/property_ids.cpp.in
+++ b/src/libcamera/property_ids.cpp.in
@@ -32,6 +32,8 @@ ${draft_controls_doc}
 
 } /* namespace draft */
 
+${vendor_controls_doc}
+
 #ifndef __DOXYGEN__
 /*
  * Keep the properties definitions hidden from doxygen as it incorrectly parses
@@ -44,6 +46,9 @@ namespace draft {
 ${draft_controls_def}
 
 } /* namespace draft */
+
+${vendor_controls_def}
+
 #endif
 
 /**
diff --git a/src/libcamera/property_ids.yaml b/src/libcamera/property_ids.yaml
index f35563842a5a..45f3609b4236 100644
--- a/src/libcamera/property_ids.yaml
+++ b/src/libcamera/property_ids.yaml
@@ -4,6 +4,7 @@
 #
 %YAML 1.1
 ---
+vendor: libcamera
 controls:
   - Location:
       type: int32_t
diff --git a/src/py/libcamera/gen-py-controls.py b/src/py/libcamera/gen-py-controls.py
index 9948c41e42b1..dfd7c179a883 100755
--- a/src/py/libcamera/gen-py-controls.py
+++ b/src/py/libcamera/gen-py-controls.py
@@ -24,45 +24,59 @@ def find_common_prefix(strings):
 def generate_py(controls, mode):
     out = ''
 
-    for ctrl in controls:
-        name, ctrl = ctrl.popitem()
-
-        if ctrl.get('draft'):
-            ns = 'libcamera::{}::draft::'.format(mode)
-            container = 'draft'
-        else:
-            ns = 'libcamera::{}::'.format(mode)
-            container = 'controls'
+    vendors_class_def = []
+    vendor_defs = []
+    vendors = []
+    for vendor, ctrl_list in controls.items():
+        for ctrls in ctrl_list:
+            name, ctrl = ctrls.popitem()
+
+            if vendor not in vendors and vendor != 'libcamera':
+                vendors_class_def.append('class Py{}Controls\n{{\n}};\n'.format(vendor))
+                vendor_defs.append('\tauto {} = py::class_<Py{}Controls>(controls, \"{}\");'.format(vendor, vendor, vendor))
+                vendors.append(vendor)
+
+            if ctrl.get('draft'):
+                ns = 'libcamera::{}::draft::'.format(mode)
+                container = 'draft'
+            elif vendor != 'libcamera':
+                ns = 'libcamera::{}::{}::'.format(mode, vendor)
+                container = vendor
+            else:
+                ns = 'libcamera::{}::'.format(mode)
+                container = 'controls'
 
-        out += f'\t{container}.def_readonly_static("{name}", static_cast<const libcamera::ControlId *>(&{ns}{name}));\n\n'
+            out += f'\t{container}.def_readonly_static("{name}", static_cast<const libcamera::ControlId *>(&{ns}{name}));\n\n'
 
-        enum = ctrl.get('enum')
-        if not enum:
-            continue
+            enum = ctrl.get('enum')
+            if not enum:
+                continue
 
-        cpp_enum = name + 'Enum'
+            cpp_enum = name + 'Enum'
 
-        out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum)
+            out += '\tpy::enum_<{}{}>({}, \"{}\")\n'.format(ns, cpp_enum, container, cpp_enum)
 
-        if mode == 'controls':
-            # Adjustments for controls
-            if name == 'LensShadingMapMode':
-                prefix = 'LensShadingMapMode'
+            if mode == 'controls':
+                # Adjustments for controls
+                if name == 'LensShadingMapMode':
+                    prefix = 'LensShadingMapMode'
+                else:
+                    prefix = find_common_prefix([e['name'] for e in enum])
             else:
+                # Adjustments for properties
                 prefix = find_common_prefix([e['name'] for e in enum])
-        else:
-            # Adjustments for properties
-            prefix = find_common_prefix([e['name'] for e in enum])
 
-        for entry in enum:
-            cpp_enum = entry['name']
-            py_enum = entry['name'][len(prefix):]
+            for entry in enum:
+                cpp_enum = entry['name']
+                py_enum = entry['name'][len(prefix):]
 
-            out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum)
+                out += '\t\t.value(\"{}\", {}{})\n'.format(py_enum, ns, cpp_enum)
 
-        out += '\t;\n\n'
+            out += '\t;\n\n'
 
-    return {'controls': out}
+    return {'controls': out,
+            'vendors_class_def': '\n'.join(vendors_class_def),
+            'vendors_defs': '\n'.join(vendor_defs)}
 
 
 def fill_template(template, data):
@@ -75,14 +89,14 @@ def fill_template(template, data):
 def main(argv):
     # Parse command line arguments
     parser = argparse.ArgumentParser()
-    parser.add_argument('-o', dest='output', metavar='file', type=str,
+    parser.add_argument('--mode', '-m', type=str, required=True,
+                        help='Mode is either "controls" or "properties"')
+    parser.add_argument('--output', '-o', metavar='file', type=str,
                         help='Output file name. Defaults to standard output if not specified.')
     parser.add_argument('input', type=str,
                         help='Input file name.')
     parser.add_argument('template', type=str,
                         help='Template file name.')
-    parser.add_argument('--mode', type=str, required=True,
-                        help='Mode is either "controls" or "properties"')
     args = parser.parse_args(argv[1:])
 
     if args.mode not in ['controls', 'properties']:
@@ -90,7 +104,10 @@ def main(argv):
         return -1
 
     data = open(args.input, 'rb').read()
-    controls = yaml.safe_load(data)['controls']
+
+    controls = {}
+    vendor = yaml.safe_load(data)['vendor']
+    controls[vendor] = yaml.safe_load(data)['controls']
 
     data = generate_py(controls, args.mode)
 
diff --git a/src/py/libcamera/py_controls_generated.cpp.in b/src/py/libcamera/py_controls_generated.cpp.in
index 18fa57d948ea..ec4b55ef2011 100644
--- a/src/py/libcamera/py_controls_generated.cpp.in
+++ b/src/py/libcamera/py_controls_generated.cpp.in
@@ -21,10 +21,13 @@ class PyDraftControls
 {
 };
 
+${vendors_class_def}
+
 void init_py_controls_generated(py::module& m)
 {
 	auto controls = py::class_<PyControls>(m, "controls");
 	auto draft = py::class_<PyDraftControls>(controls, "draft");
+${vendors_defs}
 
 ${controls}
 }
diff --git a/src/py/libcamera/py_properties_generated.cpp.in b/src/py/libcamera/py_properties_generated.cpp.in
index e49b6e91bb83..f7b5ec8c635d 100644
--- a/src/py/libcamera/py_properties_generated.cpp.in
+++ b/src/py/libcamera/py_properties_generated.cpp.in
@@ -21,10 +21,13 @@ class PyDraftProperties
 {
 };
 
+${vendors_class_def}
+
 void init_py_properties_generated(py::module& m)
 {
 	auto controls = py::class_<PyProperties>(m, "properties");
 	auto draft = py::class_<PyDraftProperties>(controls, "draft");
+${vendors_defs}
 
 ${controls}
 }
diff --git a/utils/gen-controls.py b/utils/gen-controls.py
index 1075ae302ce1..c1172cb26e7b 100755
--- a/utils/gen-controls.py
+++ b/utils/gen-controls.py
@@ -35,11 +35,12 @@ class ControlEnum(object):
 
 
 class Control(object):
-    def __init__(self, name, data):
+    def __init__(self, name, data, vendor):
         self.__name = name
         self.__data = data
         self.__enum_values = None
         self.__size = None
+        self.__vendor = vendor
 
         enum_values = data.get('enum')
         if enum_values is not None:
@@ -89,6 +90,11 @@ class Control(object):
         """Is the control a draft control"""
         return self.__data.get('draft') is not None
 
+    @property
+    def vendor(self):
+        """The vendor string, or None"""
+        return self.__vendor
+
     @property
     def name(self):
         """The control name (CamelCase)"""
@@ -145,15 +151,18 @@ ${description}
     enum_values_start = string.Template('''extern const std::array<const ControlValue, ${size}> ${name}Values = {''')
     enum_values_values = string.Template('''\tstatic_cast<int32_t>(${name}),''')
 
-    ctrls_doc = []
-    ctrls_def = []
-    draft_ctrls_doc = []
-    draft_ctrls_def = []
+    ctrls_doc = {}
+    ctrls_def = {}
     ctrls_map = []
 
     for ctrl in controls:
         id_name = snake_case(ctrl.name).upper()
 
+        vendor = 'draft' if ctrl.is_draft else ctrl.vendor
+        if vendor not in ctrls_doc:
+            ctrls_doc[vendor] = []
+            ctrls_def[vendor] = []
+
         info = {
             'name': ctrl.name,
             'type': ctrl.type,
@@ -161,11 +170,8 @@ ${description}
             'id_name': id_name,
         }
 
-        target_doc = ctrls_doc
-        target_def = ctrls_def
-        if ctrl.is_draft:
-            target_doc = draft_ctrls_doc
-            target_def = draft_ctrls_def
+        target_doc = ctrls_doc[vendor]
+        target_def = ctrls_def[vendor]
 
         if ctrl.is_enum:
             enum_doc = []
@@ -203,39 +209,68 @@ ${description}
 
         ctrls_map.append('\t{ ' + id_name + ', &' + ctrl.q_name + ' },')
 
+    vendor_ctrl_doc_sub = []
+    vendor_ctrl_template = string.Template('''
+/**
+ * \\brief Namespace for ${vendor} controls
+ */
+namespace ${vendor} {
+
+${vendor_controls_str}
+
+} /* namespace ${vendor} */''')
+
+    for vendor in [v for v in ctrls_doc.keys() if v not in ['libcamera', 'draft']]:
+        vendor_ctrl_doc_sub.append(vendor_ctrl_template.substitute({'vendor': vendor, 'vendor_controls_str': '\n\n'.join(ctrls_doc[vendor])}))
+
+    vendor_ctrl_def_sub = []
+    for vendor in [v for v in ctrls_def.keys() if v not in ['libcamera', 'draft']]:
+        vendor_ctrl_def_sub.append(vendor_ctrl_template.substitute({'vendor': vendor, 'vendor_controls_str': '\n'.join(ctrls_def[vendor])}))
+
     return {
-        'controls_doc': '\n\n'.join(ctrls_doc),
-        'controls_def': '\n'.join(ctrls_def),
-        'draft_controls_doc': '\n\n'.join(draft_ctrls_doc),
-        'draft_controls_def': '\n\n'.join(draft_ctrls_def),
+        'controls_doc': '\n\n'.join(ctrls_doc['libcamera']),
+        'controls_def': '\n'.join(ctrls_def['libcamera']),
+        'draft_controls_doc': '\n\n'.join(ctrls_doc['draft']),
+        'draft_controls_def': '\n\n'.join(ctrls_def['draft']),
         'controls_map': '\n'.join(ctrls_map),
+        'vendor_controls_doc': '\n'.join(vendor_ctrl_doc_sub),
+        'vendor_controls_def': '\n'.join(vendor_ctrl_def_sub),
     }
 
 
-def generate_h(controls):
+def generate_h(controls, mode):
     enum_template_start = string.Template('''enum ${name}Enum {''')
     enum_value_template = string.Template('''\t${name} = ${value},''')
     enum_values_template = string.Template('''extern const std::array<const ControlValue, ${size}> ${name}Values;''')
     template = string.Template('''extern const Control<${type}> ${name};''')
 
-    ctrls = []
-    draft_ctrls = []
-    ids = []
-    id_value = 1
+    ctrls = {}
+    ids = {}
+    id_value = {}
 
     for ctrl in controls:
         id_name = snake_case(ctrl.name).upper()
 
-        ids.append('\t' + id_name + ' = ' + str(id_value) + ',')
+        vendor = 'draft' if ctrl.is_draft else ctrl.vendor
+        if vendor not in ctrls:
+            ids[vendor] = []
+            id_value[vendor] = 1
+            ctrls[vendor] = []
+
+        # Core and draft controls use the same ID value
+        target_ids = ids['libcamera'] if vendor in ['libcamera', 'draft'] else ids[vendor]
+        target_ids.append('\t' + id_name + ' = ' + str(id_value[vendor]) + ',')
 
         info = {
             'name': ctrl.name,
             'type': ctrl.type,
         }
 
-        target_ctrls = ctrls
+        target_ctrls = ctrls['libcamera']
         if ctrl.is_draft:
-            target_ctrls = draft_ctrls
+            target_ctrls = ctrls['draft']
+        elif vendor != 'libcamera':
+            target_ctrls = ctrls[vendor]
 
         if ctrl.is_enum:
             target_ctrls.append(enum_template_start.substitute(info))
@@ -257,12 +292,35 @@ def generate_h(controls):
             target_ctrls.append(enum_values_template.substitute(values_info))
 
         target_ctrls.append(template.substitute(info))
-        id_value += 1
+        id_value[vendor] += 1
+
+    vendor_template = string.Template('''
+namespace ${vendor} {
+
+#define LIBCAMERA_HAS_${vendor_def}_VENDOR_${mode}
+
+enum {
+${vendor_enums}
+};
+
+${vendor_controls}
+
+} /* namespace ${vendor} */
+''')
+
+    vendor_sub = []
+    for vendor in [v for v in ctrls.keys() if v not in ['libcamera', 'draft']]:
+        vendor_sub.append(vendor_template.substitute({'mode': mode.upper(),
+                                                      'vendor': vendor,
+                                                      'vendor_def': vendor.upper(),
+                                                      'vendor_enums': '\n'.join(ids[vendor]),
+                                                      'vendor_controls': '\n'.join(ctrls[vendor])}))
 
     return {
-        'ids': '\n'.join(ids),
-        'controls': '\n'.join(ctrls),
-        'draft_controls': '\n'.join(draft_ctrls)
+        'ids': '\n'.join(ids['libcamera']),
+        'controls': '\n'.join(ctrls['libcamera']),
+        'draft_controls': '\n'.join(ctrls['draft']),
+        'vendor_controls': '\n'.join(vendor_sub)
     }
 
 
@@ -278,22 +336,26 @@ def main(argv):
 
     # Parse command line arguments
     parser = argparse.ArgumentParser()
-    parser.add_argument('-o', dest='output', metavar='file', type=str,
+    parser.add_argument('--mode', '-m', type=str, required=True, choices=['controls', 'properties'],
+                        help='Mode of operation')
+    parser.add_argument('--output', '-o', metavar='file', type=str,
                         help='Output file name. Defaults to standard output if not specified.')
     parser.add_argument('input', type=str,
                         help='Input file name.')
     parser.add_argument('template', type=str,
                         help='Template file name.')
+
     args = parser.parse_args(argv[1:])
 
     data = open(args.input, 'rb').read()
+    vendor = yaml.safe_load(data)['vendor']
     controls = yaml.safe_load(data)['controls']
-    controls = [Control(*ctrl.popitem()) for ctrl in controls]
+    controls = [Control(*ctrl.popitem(), vendor) for ctrl in controls]
 
     if args.template.endswith('.cpp.in'):
         data = generate_cpp(controls)
     elif args.template.endswith('.h.in'):
-        data = generate_h(controls)
+        data = generate_h(controls, args.mode)
     else:
         raise RuntimeError('Unknown template type')
 
