Repository: zigpy/zha-device-handlers (python)

Issue:
[Device Support Request] NHPB/SHUTTER/1 by Schneider Electric
**Describe the solution you'd like**
[This is a roller blind switch from Schneider Electric.](https://zigbee.blakadder.com/Schneider_Electric_S520567.html
)
The device can be triggered for open/close/stop action on Home-Assistant through Zigpy but I could not find a way to calibrate it.

<details>
<!-- Device signature can be acquired by clicking on the "Zigbee Device Signature" button in the device settings view -->
<summary>Device signature</summary>

```yaml
{
  "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4190, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
  "endpoints": {
    "5": {
      "profile_id": 260,
      "device_type": "0x0202",
      "in_clusters": [
        "0x0000",
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0102",
        "0x0b05"
      ],
      "out_clusters": [
        "0x0019"
      ]
    },
    "21": {
      "profile_id": 260,
      "device_type": "0x0104",
      "in_clusters": [
        "0x0000",
        "0x0003",
        "0x0b05",
        "0xff17"
      ],
      "out_clusters": [
        "0x0003",
        "0x0004",
        "0x0005",
        "0x0006",
        "0x0008",
        "0x0102"
      ]
    },
    "242": {
      "profile_id": 41440,
      "device_type": "0x0061",
      "in_clusters": [],
      "out_clusters": [
        "0x0021"
      ]
    }
  },
  "manufacturer": "Schneider Electric",
  "model": "NHPB/SHUTTER/1",
  "class": "zigpy.device.Device"
}
```

</details>

<details>
<!-- Diagnostic information can be acquired by clicking on the "Download Diagnostics" button in the device settings view -->
<summary>Diagnostic information</summary>

```yaml
{
  "home_assistant": {
    "installation_type": "Home Assistant Supervised",
    "version": "2022.8.1",
    "dev": false,
    "hassio": true,
    "virtualenv": false,
    "python_version": "3.10.5",
    "docker": true,
    "arch": "x86_64",
    "timezone": "Europe/Paris",
    "os_name": "Linux",
    "os_version": "5.4.0-1042205121620-generic",
    "supervisor": "2022.07.0",
    "host_os": null,
    "docker_version": "20.10.16",
    "chassis": null,
    "run_as_root": true
  },
  "custom_components": {
    "xiaomi_cloud_map_extractor": {
      "version": "v2.2.0",
      "requirements": [
        "pillow",
        "pybase64",
        "python-miio",
        "requests",
        "pycryptodome"
      ]
    },
    "adaptive_lighting": {
      "version": "1.0.14",
      "requirements": []
    },
    "hacs": {
      "version": "1.26.2",
      "requirements": [
        "aiogithubapi>=22.2.4"
      ]
    },
    "zha_toolkit": {
      "version": "v0.8.11",
      "requirements": []
    },
    "ytube_music_player": {
      "version": "20220205.01",
      "requirements": [
        "ytmusicapi==0.19.5",
        "pytube==10.5.1",
        "integrationhelper==0.2.2"
      ]
    }
  },
  "integration_manifest": {
    "domain": "zha",
    "name": "Zigbee Home Automation",
    "config_flow": true,
    "documentation": "https://www.home-assistant.io/integrations/zha",
    "requirements": [
      "bellows==0.31.2",
      "pyserial==3.5",
      "pyserial-asyncio==0.6",
      "zha-quirks==0.0.78",
      "zigpy-deconz==0.18.0",
      "zigpy==0.48.0",
      "zigpy-xbee==0.15.0",
      "zigpy-zigate==0.9.0",
      "zigpy-znp==0.8.1"
    ],
    "usb": [
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*2652*",
        "known_devices": [
          "slae.sh cc2652rb stick"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*sonoff*plus*",
        "known_devices": [
          "sonoff zigbee dongle plus"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*tubeszb*",
        "known_devices": [
          "TubesZB Coordinator"
        ]
      },
      {
        "vid": "1A86",
        "pid": "7523",
        "description": "*zigstar*",
        "known_devices": [
          "ZigStar Coordinators"
        ]
      },
      {
        "vid": "1CF1",
        "pid": "0030",
        "description": "*conbee*",
        "known_devices": [
          "Conbee II"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8A2A",
        "description": "*zigbee*",
        "known_devices": [
          "Nortek HUSBZB-1"
        ]
      },
      {
        "vid": "0403",
        "pid": "6015",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate+"
        ]
      },
      {
        "vid": "10C4",
        "pid": "EA60",
        "description": "*zigate*",
        "known_devices": [
          "ZiGate"
        ]
      },
      {
        "vid": "10C4",
        "pid": "8B34",
        "description": "*bv 2010/10*",
        "known_devices": [
          "Bitron Video AV2010/10"
        ]
      }
    ],
    "codeowners": [
      "@dmulcahey",
      "@adminiuga",
      "@puddly"
    ],
    "zeroconf": [
      {
        "type": "_esphomelib._tcp.local.",
        "name": "tube*"
      },
      {
        "type": "_zigate-zigbee-gateway._tcp.local.",
        "name": "*zigate*"
      }
    ],
    "after_dependencies": [
      "onboarding",
      "usb",
      "zeroconf"
    ],
    "iot_class": "local_polling",
    "loggers": [
      "aiosqlite",
      "bellows",
      "crccheck",
      "pure_pcapy3",
      "zhaquirks",
      "zigpy",
      "zigpy_deconz",
      "zigpy_xbee",
      "zigpy_zigate",
      "zigpy_znp"
    ],
    "is_built_in": true
  },
  "data": {
    "ieee": "**REDACTED**",
    "nwk": 8658,
    "manufacturer": "Schneider Electric",
    "model": "NHPB/SHUTTER/1",
    "name": "Schneider Electric NHPB/SHUTTER/1",
    "quirk_applied": false,
    "quirk_class": "zigpy.device.Device",
    "manufacturer_code": 4190,
    "power_source": "Mains",
    "lqi": 191,
    "rssi": -70,
    "last_seen": "2022-08-07T20:51:16",
    "available": true,
    "device_type": "Router",
    "signature": {
      "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4190, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)",
      "endpoints": {
        "5": {
          "profile_id": 260,
          "device_type": "0x0202",
          "in_clusters": [
            "0x0000",
            "0x0003",
            "0x0004",
            "0x0005",
            "0x0102",
            "0x0b05"
          ],
          "out_clusters": [
            "0x0019"
          ]
        },
        "21": {
          "profile_id": 260,
          "device_type": "0x0104",
          "in_clusters": [
            "0x0000",
            "0x0003",
            "0x0b05",
            "0xff17"
          ],
          "out_clusters": [
            "0x0003",
            "0x0004",
            "0x0005",
            "0x0006",
            "0x0008",
            "0x0102"
          ]
        },
        "242": {
          "profile_id": 41440,
          "device_type": "0x0061",
          "in_clusters": [],
          "out_clusters": [
            "0x0021"
          ]
        }
      }
    },
    "active_coordinator": false,
    "entities": [
      {
        "entity_id": "button.schneider_electric_nhpb_shutter_1_213249fe_identify",
        "name": "Schneider Electric NHPB/SHUTTER/1"
      },
      {
        "entity_id": "cover.schneider_electric_nhpb_shutter_1_213249fe_window_covering",
        "name": "Schneider Electric NHPB/SHUTTER/1"
      }
    ],
    "neighbors": [
      {
        "device_type": "Coordinator",
        "rx_on_when_idle": "On",
        "relationship": "Parent",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x0000",
        "permit_joining": "Unknown",
        "depth": "0",
        "lqi": "156"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x1DE7",
        "permit_joining": "Unknown",
        "depth": "15",
        "lqi": "88"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0x6EFE",
        "permit_joining": "Unknown",
        "depth": "15",
        "lqi": "70"
      },
      {
        "device_type": "Router",
        "rx_on_when_idle": "On",
        "relationship": "Sibling",
        "extended_pan_id": "**REDACTED**",
        "ieee": "**REDACTED**",
        "nwk": "0xEC3C",
        "permit_joining": "Unknown",
        "depth": "15",
        "lqi": "100"
      }
    ],
    "endpoint_names": [
      {
        "name": "WINDOW_COVERING_DEVICE"
      },
      {
        "name": "DIMMER_SWITCH"
      },
      {
        "name": "unknown 97 device_type of 0xa1e0 profile id"
      }
    ],
    "user_given_name": "Interrupteur Fen\u00eatre Bureau Chambre Invit\u00e9",
    "device_reg_id": "d626a7278aca3b5073ef76a8932d705c",
    "area_id": "be619747352e431fa859a0c71eda0311",
    "cluster_details": {
      "5": {
        "device_type": {
          "name": "WINDOW_COVERING_DEVICE",
          "id": 514
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {
              "0x0004": {
                "attribute_name": "manufacturer",
                "value": "Schneider Electric"
              },
              "0x0005": {
                "attribute_name": "model",
                "value": "NHPB/SHUTTER/1"
              }
            },
            "unsupported_attributes": {}
          },
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0102": {
            "endpoint_attribute": "window_covering",
            "attributes": {
              "0x0000": {
                "attribute_name": "window_covering_type",
                "value": 8
              },
              "0x0007": {
                "attribute_name": "config_status",
                "value": 3
              },
              "0x0008": {
                "attribute_name": "current_position_lift_percentage",
                "value": 8
              },
              "0x0017": {
                "attribute_name": "window_covering_mode",
                "value": 8
              }
            },
            "unsupported_attributes": {
              "0x0001": {
                "attribute_name": "physical_closed_limit_lift"
              },
              "0x0003": {
                "attribute_name": "current_position_lift"
              },
              "0x0004": {
                "attribute_name": "current_position_tilt"
              },
              "0x0012": {
                "attribute_name": "installed_open_limit_tilt"
              },
              "0x0011": {
                "attribute_name": "installed_closed_limit_lift"
              },
              "0x0015": {
                "attribute_name": "acceleration_time_lift"
              },
              "0x0013": {
                "attribute_name": "installed_closed_limit_tilt"
              },
              "0x0017": {
                "attribute_name": "window_covering_mode"
              }
            }
          },
          "0x0b05": {
            "endpoint_attribute": "diagnostic",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0019": {
            "endpoint_attribute": "ota",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      },
      "21": {
        "device_type": {
          "name": "DIMMER_SWITCH",
          "id": 260
        },
        "profile_id": 260,
        "in_clusters": {
          "0x0000": {
            "endpoint_attribute": "basic",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0b05": {
            "endpoint_attribute": "diagnostic",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0xff17": {
            "endpoint_attribute": "manufacturer_specific",
            "attributes": {},
            "unsupported_attributes": {}
          }
        },
        "out_clusters": {
          "0x0003": {
            "endpoint_attribute": "identify",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0004": {
            "endpoint_attribute": "groups",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0005": {
            "endpoint_attribute": "scenes",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0006": {
            "endpoint_attribute": "on_off",
            "attributes": {},
            "unsupported_attributes": {}
          },
          "0x0008": {
            "endpoint_attribute": "level",
            "attributes": {},
            "unsupported_attributes": {
              "0x0003": {
                "attribute_name": "max_level"
              }
            }
          },
          "0x0102": {
            "endpoint_attribute": "window_covering",
            "attributes": {},
            "unsupported_attributes": {
              "0x0007": {
                "attribute_name": "config_status"
              }
            }
          }
        }
      },
      "242": {
        "device_type": {
          "name": "unknown",
          "id": 97
        },
        "profile_id": 41440,
        "in_clusters": {},
        "out_clusters": {
          "0x0021": {
            "endpoint_attribute": "green_power",
            "attributes": {},
            "unsupported_attributes": {}
          }
        }
      }
    }
  }
}
```

</details>

<details>
<summary>Additional logs</summary>

```
Paste any additional debug logs here.
Don't remove the extra line breaks outside the ``` marks.
```

</details>

**Additional context**

I can see through HA that there are also cluster name with different `endpoint id`.


Pull request:
Convert Schneider Electric shutters to quirks v2, expose entities
## Proposed change
- Convert to quirks v2
- Define missing attributes using spec doc
- Add support for extra models (NHPB/SHUTTER/1 and PUCK/SHUTTER/1)
- Expose extra entities to home assistant

Fixes #1685
Supersedes #3939

## Additional information
#1705 
[ZB Spec - Micro Module Shutter Blinds - 110422.pdf](https://github.com/user-attachments/files/20774274/ZB.Spec.-.Micro.Module.Shutter.Blinds.-.110422.pdf)


## Checklist
<!--
  Put an 'x' in all boxes that apply.
  Note: You do not need to tick all boxes before creating a PR.
-->

- [ ] The changes are tested and work correctly
- [ ] `pre-commit` checks pass / the code has been formatted using Black
- [ ] Tests have been added to verify that the new code works


Patch:
diff --git a/zhaquirks/nimly/lock.py b/zhaquirks/nimly/lock.py
index b65be79..0bfb406 100644
--- a/zhaquirks/nimly/lock.py
+++ b/zhaquirks/nimly/lock.py
@@ -1,6 +1,13 @@
 """Device handler for Nimly Smart Locks."""
 
+from typing import Final
+
+from zigpy.quirks import CustomCluster
 from zigpy.quirks.v2 import QuirkBuilder
+from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass
+import zigpy.types as t
+from zigpy.zcl.clusters.closures import DoorLock
+from zigpy.zcl.foundation import ZCLAttributeDef
 from zigpy.zdo.types import NodeDescriptor
 
 from zhaquirks import DoublingPowerConfigurationCluster
@@ -25,11 +32,117 @@ NIMLY_LOCK_NODE_DESCRIPTOR = NodeDescriptor(
 )
 
 
+class NimlyDoorLock(CustomCluster, DoorLock):
+    """Nimly Door Lock cluster."""
+
+    class AttributeDefs(DoorLock.AttributeDefs):
+        """Nimly Door Lock attribute definitions."""
+
+        nimly_last_lock_unlock_source: Final = ZCLAttributeDef(
+            id=0x100,
+            type=t.bitmap32,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        nimly_last_pin_code: Final = ZCLAttributeDef(
+            id=0x101,
+            type=t.LVBytes,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+
+
+def last_action_source_converter(value: int) -> str:
+    """Extract last action source value."""
+    value_hex = hex(value)[2:].zfill(8)
+    sources = {
+        "00": "zigbee",
+        "02": "keypad",
+        "03": "fingerprint",
+        "04": "rfid",
+        "0a": "self",
+    }
+    return sources.get(value_hex[0:2])
+
+
+def last_action_converter(value: int) -> str:
+    """Extract last action value."""
+    value_hex = hex(value)[2:].zfill(8)
+    actions = {
+        "01": "lock",
+        "02": "unlock",
+    }
+    return actions.get(value_hex[2:4])
+
+
+def last_action_user_converter(value: int) -> int:
+    """Extract last action user value."""
+    value_hex = hex(value)[2:].zfill(8)
+    user_id = int(value_hex[4:8], 16)
+    return user_id
+
+
 (
     QuirkBuilder(NIMLY, "EasyFingerTouch")
     .applies_to(NIMLY, "EasyCodeTouch")
     .applies_to(NIMLY, "easyCodeTouch_v1")
     .node_descriptor(NIMLY_LOCK_NODE_DESCRIPTOR)
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
+        unique_id_suffix="last_action_source",
+        attribute_converter=last_action_source_converter,
+        device_class=SensorDeviceClass.ENUM,
+        translation_key="last_action_source",
+        fallback_name="Last action source",
+    )
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
+        unique_id_suffix="last_action",
+        attribute_converter=last_action_converter,
+        device_class=SensorDeviceClass.ENUM,
+        translation_key="last_action",
+        fallback_name="Last action",
+    )
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
+        unique_id_suffix="last_action_user",
+        attribute_converter=last_action_user_converter,
+        translation_key="last_action_user",
+        fallback_name="Last action user",
+    )
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_pin_code.name,
+        unique_id_suffix="last_pin_code",
+        attribute_converter=lambda value: value.hex(),
+        initially_disabled=True,
+        translation_key="last_pin_code",
+        fallback_name="Last pin code",
+    )
+    .switch(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.auto_relock_time.name,
+        translation_key="auto_relock",
+        fallback_name="Auto relock",
+    )
+    .number(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.sound_volume.name,
+        min_value=0,
+        max_value=2,
+        step=1,
+        translation_key="sound_volume",
+        fallback_name="Sound volume",
+    )
     .add_to_registry()
 )
 
@@ -41,5 +154,62 @@ NIMLY_LOCK_NODE_DESCRIPTOR = NodeDescriptor(
     .applies_to(NIMLY, "NimlyIn")
     .node_descriptor(NIMLY_LOCK_NODE_DESCRIPTOR)
     .replaces(DoublingPowerConfigurationCluster, endpoint_id=11)
+    .replaces(NimlyDoorLock, endpoint_id=11)
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
+        unique_id_suffix="last_action_source",
+        attribute_converter=last_action_source_converter,
+        device_class=SensorDeviceClass.ENUM,
+        translation_key="last_action_source",
+        fallback_name="Last action source",
+    )
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
+        unique_id_suffix="last_action",
+        attribute_converter=last_action_converter,
+        device_class=SensorDeviceClass.ENUM,
+        translation_key="last_action",
+        fallback_name="Last action",
+    )
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
+        unique_id_suffix="last_action_user",
+        attribute_converter=last_action_user_converter,
+        translation_key="last_action_user",
+        fallback_name="Last action user",
+    )
+    .sensor(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_pin_code.name,
+        unique_id_suffix="last_pin_code",
+        attribute_converter=lambda value: value.hex(),
+        initially_disabled=True,
+        translation_key="last_pin_code",
+        fallback_name="Last pin code",
+    )
+    .switch(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.auto_relock_time.name,
+        translation_key="auto_relock",
+        fallback_name="Auto relock",
+    )
+    .number(
+        endpoint_id=11,
+        cluster_id=NimlyDoorLock.cluster_id,
+        attribute_name=NimlyDoorLock.AttributeDefs.sound_volume.name,
+        min_value=0,
+        max_value=2,
+        step=1,
+        translation_key="sound_volume",
+        fallback_name="Sound volume",
+    )
     .add_to_registry()
 )
diff --git a/zhaquirks/schneiderelectric/__init__.py b/zhaquirks/schneiderelectric/__init__.py
index 9f8246d..db10abd 100644
--- a/zhaquirks/schneiderelectric/__init__.py
+++ b/zhaquirks/schneiderelectric/__init__.py
@@ -155,6 +155,20 @@ class SEDimmingCurve(t.enum8):
     Exponential = 2  # Not supported in current FW, but defined in the spec
 
 
+class SESunProtectionStatus(t.bitmap8):
+    """Window covering protetion status."""
+
+    NotActive = 0
+    Active = 1
+
+
+class SESunProtectionSensor(t.bitmap8):
+    """Window covering protetion status."""
+
+    NotConnected = 0
+    Connected = 1
+
+
 class SEBallast(CustomCluster, Ballast):
     """Schneider Electric Ballast cluster."""
 
@@ -180,49 +194,85 @@ class SEWindowCovering(CustomCluster, WindowCovering):
     class AttributeDefs(WindowCovering.AttributeDefs):
         """Attribute definitions."""
 
-        unknown_attribute_65533: Final = ZCLAttributeDef(
-            id=0xFFFD,
-            type=t.uint16_t,
-            is_manufacturer_specific=True,
-        )
-        lift_duration: Final = ZCLAttributeDef(
+        # Obsolete, use se_lift_drive_up_time and se_lift_drive_down_time instead.
+        se_lift_duration: Final = ZCLAttributeDef(
             id=0xE000,
             type=t.uint16_t,
+            access="rw",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57360: Final = ZCLAttributeDef(
+        se_sun_protection_status: Final = ZCLAttributeDef(
             id=0xE010,
-            type=t.bitmap8,
+            type=SESunProtectionStatus,
+            access="r",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57362: Final = ZCLAttributeDef(
+        # SunProtectionIlluminanceThreshold = 10,000 x log10 Illuminance.
+        # Where 1 lx <= Illuminance <=3.576 Mlx, corresponding to a SunProtectionIlluminanceThreshold in the range 0 to 0xfffe.
+        # A value of 0xffff indicates that this attribute is not valid.
+        se_sun_protection_illuminance_thershold: Final = ZCLAttributeDef(
             id=0xE012,
             type=t.uint16_t,
+            access="rw",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57363: Final = ZCLAttributeDef(
+        se_sun_protection_sensor: Final = ZCLAttributeDef(
             id=0xE013,
-            type=t.bitmap8,
+            type=SESunProtectionSensor,
+            access="rw",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57364: Final = ZCLAttributeDef(
+        # Driving time from fully close to fully open state in 1/10 seconds
+        se_lift_drive_up_time: Final = ZCLAttributeDef(
             id=0xE014,
             type=t.uint16_t,
+            access="rw",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57365: Final = ZCLAttributeDef(
+        # Driving time from fully open to fully close state in 1/10 seconds
+        se_lift_drive_down_time: Final = ZCLAttributeDef(
             id=0xE015,
             type=t.uint16_t,
+            access="rw",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57366: Final = ZCLAttributeDef(
+        # Time from fully open to fully close tilt position in 1/100 seconds.
+        # This time is also taken as base for calculation of step size in Schneider manufacture specific command StopOrStepLiftPercentage.
+        # If set to 0, WindosCoveringType attribute is automatically set to 0 (lift only).
+        se_tilt_open_close_and_step_time: Final = ZCLAttributeDef(
             id=0xE016,
             type=t.uint16_t,
+            access="rw",
             is_manufacturer_specific=True,
         )
-        unknown_attribute_57367: Final = ZCLAttributeDef(
+        # Tilt position in percent adopted by tilt after receiving go to lift percentage command.
+        # Values 0-100 are absolute position of tilt with following meaning:
+        # 100: Position of tilt when shutter is moving up (usually up).
+        # 0: Position of tilt when shutter is moving down (usually down).
+        # 255: No action after command.
+        # 101-254: Tilt position before movement is restored.
+        se_tilt_position_percentage_after_move_to_level: Final = ZCLAttributeDef(
             id=0xE017,
             type=t.uint8_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_cluster_revision: Final = ZCLAttributeDef(
+            id=0xFFFD,
+            type=t.uint16_t,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+
+    class ServerCommandDefs(WindowCovering.ServerCommandDefs):
+        """Server command definitions."""
+
+        se_stop_or_step_lift_percentage: Final = foundation.ZCLCommandDef(
+            id=0x80,
+            schema={
+                "direction": t.uint8_t,
+                "step_value": t.uint8_t,
+            },
             is_manufacturer_specific=True,
         )
 
@@ -305,11 +355,11 @@ class SESwitchAction(t.enum8):
     NotUsed = 0x7F
 
 
-class SESpecific(CustomCluster):
-    """Schneider Electric manufacturer specific cluster."""
+class SESwitchConfiguration(CustomCluster):
+    """Schneider Electric manufacturer specific switch configuration cluster."""
 
-    name = "Schneider Electric Manufacturer Specific"
-    ep_attribute = "schneider_electric_manufacturer"
+    name = "Schneider Electric Manufacturer Specific Switch Configuration"
+    ep_attribute = "switch_configuration"
     cluster_id = 0xFF17
 
     class AttributeDefs(BaseAttributeDefs):
diff --git a/zhaquirks/schneiderelectric/dimmers.py b/zhaquirks/schneiderelectric/dimmers.py
index 098a113..969a168 100644
--- a/zhaquirks/schneiderelectric/dimmers.py
+++ b/zhaquirks/schneiderelectric/dimmers.py
@@ -7,7 +7,7 @@ from zhaquirks.schneiderelectric import (
     SEBallast,
     SEBasic,
     SEOnOff,
-    SESpecific,
+    SESwitchConfiguration,
 )
 
 (
@@ -19,7 +19,7 @@ from zhaquirks.schneiderelectric import (
     .replaces(SEBallast, endpoint_id=3)
     .replaces(SEOnOff, endpoint_id=3)
     .replaces(SEBasic, endpoint_id=21)
-    .replaces(SESpecific, endpoint_id=21)
+    .replaces(SESwitchConfiguration, endpoint_id=21)
     .add_to_registry()
 )
 
@@ -29,6 +29,6 @@ from zhaquirks.schneiderelectric import (
     .replaces(SEBasic)
     .replaces(SEOnOff)
     .replaces(SEBasic, endpoint_id=21)
-    .replaces(SESpecific, endpoint_id=21)
+    .replaces(SESwitchConfiguration, endpoint_id=21)
     .add_to_registry()
 )
diff --git a/zhaquirks/schneiderelectric/shutters.py b/zhaquirks/schneiderelectric/shutters.py
index 8af61b5..7550f69 100644
--- a/zhaquirks/schneiderelectric/shutters.py
+++ b/zhaquirks/schneiderelectric/shutters.py
@@ -1,117 +1,131 @@
 """Quirks for Schneider Electric shutters."""
 
-from zigpy.profiles import zha
-from zigpy.quirks import CustomDevice
-from zigpy.zcl.clusters.closures import WindowCovering
-from zigpy.zcl.clusters.general import (
-    Basic,
-    Groups,
-    Identify,
-    LevelControl,
-    OnOff,
-    Ota,
-    Scenes,
-)
-from zigpy.zcl.clusters.homeautomation import Diagnostic
+from zigpy.quirks.v2 import QuirkBuilder
+from zigpy.quirks.v2.homeassistant import UnitOfTime
+from zigpy.zcl import ClusterType
 
-from zhaquirks.const import (
-    DEVICE_TYPE,
-    ENDPOINTS,
-    INPUT_CLUSTERS,
-    MODELS_INFO,
-    OUTPUT_CLUSTERS,
-    PROFILE_ID,
-)
 from zhaquirks.schneiderelectric import (
     SE_MANUF_NAME,
     SEBasic,
-    SESpecific,
+    SESwitchAction,
+    SESwitchConfiguration,
+    SESwitchIndication,
     SEWindowCovering,
 )
 
+(
+    QuirkBuilder(SE_MANUF_NAME, "1GANG/SHUTTER/1")
+    .applies_to(SE_MANUF_NAME, "NHPB/SHUTTER/1")
+    .replaces(SEBasic, endpoint_id=5)
+    .replaces(SEWindowCovering, endpoint_id=5)
+    .replaces(SEBasic, endpoint_id=21)
+    .replaces(SESwitchConfiguration, endpoint_id=21)
+    .replaces(SEWindowCovering, endpoint_id=21, cluster_type=ClusterType.Client)
+    .enum(
+        endpoint_id=21,
+        cluster_id=SESwitchConfiguration.cluster_id,
+        attribute_name=SESwitchConfiguration.AttributeDefs.se_switch_indication.name,
+        enum_class=SESwitchIndication,
+        translation_key="switch_indication",
+        fallback_name="Switch indication",
+    )
+    .enum(
+        endpoint_id=21,
+        cluster_id=SESwitchConfiguration.cluster_id,
+        attribute_name=SESwitchConfiguration.AttributeDefs.se_switch_actions.name,
+        enum_class=SESwitchAction,
+        translation_key="switch_actions",
+        fallback_name="Switch actions",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_up_time.name,
+        unit=UnitOfTime.SECONDS,
+        multiplier=0.1,
+        min_value=1,
+        max_value=300,
+        step=0.1,
+        translation_key="lift_drive_up_time",
+        fallback_name="Lift drive up time",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_down_time.name,
+        unit=UnitOfTime.SECONDS,
+        multiplier=0.1,
+        min_value=1,
+        max_value=300,
+        step=0.1,
+        translation_key="lift_drive_down_time",
+        fallback_name="Lift drive down time",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_tilt_open_close_and_step_time.name,
+        unit=UnitOfTime.SECONDS,
+        multiplier=0.01,
+        min_value=0,
+        max_value=30,
+        step=0.01,
+        translation_key="tilt_open_close_and_step_time",
+        fallback_name="Tilt open close and step time",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_tilt_position_percentage_after_move_to_level.name,
+        min_value=0,
+        max_value=255,
+        step=1,
+        translation_key="tilt_position_percentage_after_move_to_level",
+        fallback_name="Tilt position percentage after move to level",
+    )
+    .add_to_registry()
+)
 
-class OneGangShutter1(CustomDevice):
-    """1GANG/SHUTTER/1 from Schneider Electric."""
-
-    signature = {
-        MODELS_INFO: [
-            (SE_MANUF_NAME, "1GANG/SHUTTER/1"),
-        ],
-        ENDPOINTS: {
-            # <SimpleDescriptor endpoint=5, profile=260, device_type=514,
-            # device_version=0,
-            # input_clusters=[0, 3, 4, 5, 258, 2821],
-            # output_clusters=[25]>
-            5: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    WindowCovering.cluster_id,
-                    Diagnostic.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [Ota.cluster_id],
-            },
-            # <SimpleDescriptor endpoint=21, profile=260, device_type=260,
-            # device_version=0,
-            # input_clusters=[0, 3, 2821, 65303],
-            # output_clusters=[3, 5, 6, 8, 25, 258]>
-            21: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    Identify.cluster_id,
-                    Diagnostic.cluster_id,
-                    SESpecific.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Identify.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    Ota.cluster_id,
-                    WindowCovering.cluster_id,
-                ],
-            },
-        },
-    }
-
-    replacement = {
-        ENDPOINTS: {
-            5: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
-                INPUT_CLUSTERS: [
-                    SEBasic,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    SEWindowCovering,
-                    Diagnostic.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [Ota.cluster_id],
-            },
-            21: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
-                INPUT_CLUSTERS: [
-                    SEBasic,
-                    Identify.cluster_id,
-                    Diagnostic.cluster_id,
-                    SESpecific,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Identify.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    Ota.cluster_id,
-                    SEWindowCovering,
-                ],
-            },
-        }
-    }
+(
+    QuirkBuilder(SE_MANUF_NAME, "PUCK/SHUTTER/1")
+    .replaces(SEBasic, endpoint_id=5)
+    .replaces(SEWindowCovering, endpoint_id=5)
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_up_time.name,
+        unit=UnitOfTime.SECONDS,
+        multiplier=0.1,
+        translation_key="lift_drive_up_time",
+        fallback_name="Lift drive up time",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_down_time.name,
+        unit=UnitOfTime.SECONDS,
+        multiplier=0.1,
+        translation_key="lift_drive_down_time",
+        fallback_name="Lift drive down time",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_tilt_open_close_and_step_time.name,
+        unit=UnitOfTime.SECONDS,
+        multiplier=0.01,
+        translation_key="tilt_open_close_and_step_time",
+        fallback_name="Tilt open close and step time",
+    )
+    .number(
+        endpoint_id=5,
+        cluster_id=SEWindowCovering.cluster_id,
+        attribute_name=SEWindowCovering.AttributeDefs.se_tilt_position_percentage_after_move_to_level.name,
+        min_value=0,
+        max_value=255,
+        step=1,
+        translation_key="tilt_position_percentage_after_move_to_level",
+        fallback_name="Tilt position percentage after move to level",
+    )
+    .add_to_registry()
+)


Test patch:
diff --git a/tests/test_nimly.py b/tests/test_nimly.py
new file mode 100644
index 0000000..43f0767
--- /dev/null
+++ b/tests/test_nimly.py
@@ -0,0 +1,46 @@
+"""Tests for Nimly lock quirks."""
+
+import zhaquirks
+from zhaquirks.nimly.lock import (
+    last_action_converter,
+    last_action_source_converter,
+    last_action_user_converter,
+)
+
+zhaquirks.setup()
+
+
+def test_last_action_source_converter() -> None:
+    """Test the last action source converter for Nimly Door Lock cluster."""
+    assert last_action_source_converter(0x00020001) == "zigbee", (
+        "Expected 'zigbee' for value 0x00010001"
+    )
+    assert last_action_source_converter(0x03010001) == "fingerprint", (
+        "Expected 'fingerprint' for value 0x03010001"
+    )
+    assert last_action_source_converter(0x99010001) is None, (
+        "Expected None for value 0x99010001"
+    )
+
+
+def test_last_action_converter() -> None:
+    """Test the last action converter for Nimly Door Lock cluster."""
+    assert last_action_converter(0x0A010001) == "lock", (
+        "Expected 'lock' for value 0x0a010001"
+    )
+    assert last_action_converter(0x00020001) == "unlock", (
+        "Expected 'unlock' for value 0x00020001"
+    )
+    assert last_action_converter(0x01030001) is None, (
+        "Expected None for value 0x01030001"
+    )
+
+
+def test_last_action_user_converter() -> None:
+    """Test the last action user converter for Nimly Door Lock cluster."""
+    assert last_action_user_converter(0x01020001) == 1, (
+        "Expected user ID 1 for value 0x01020001"
+    )
+    assert last_action_user_converter(0x02020010) == 16, (
+        "Expected user ID 16 for value 0x02020010"
+    )
diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py
index 4d43c58..00c8cc0 100644
--- a/tests/test_schneiderelectric.py
+++ b/tests/test_schneiderelectric.py
@@ -8,77 +8,19 @@ from zigpy.zcl.clusters.closures import WindowCovering
 from zigpy.zcl.clusters.smartenergy import Metering
 
 from tests.common import ClusterListener
+from zhaquirks.schneiderelectric import SE_MANUF_NAME
 import zhaquirks.schneiderelectric.outlet
-import zhaquirks.schneiderelectric.shutters
 
 zhaquirks.setup()
 
 
-def test_1gang_shutter_1_signature(assert_signature_matches_quirk):
-    """Test signature."""
-    signature = {
-        "node_descriptor": (
-            "NodeDescriptor(logical_type=<LogicalType.Router: 1>, "
-            "complex_descriptor_available=0, user_descriptor_available=0, reserved=0, "
-            "aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, "
-            "mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered"
-            "|RxOnWhenIdle|AllocateAddress: 142>, manufacturer_code=4190, "
-            "maximum_buffer_size=82, maximum_incoming_transfer_size=82, "
-            "server_mask=10752, maximum_outgoing_transfer_size=82, "
-            "descriptor_capability_field=<DescriptorCapability.NONE: 0>, "
-            "*allocate_address=True, *is_alternate_pan_coordinator=False, "
-            "*is_coordinator=False, *is_end_device=False, "
-            "*is_full_function_device=True, *is_mains_powered=True, "
-            "*is_receiver_on_when_idle=True, *is_router=True, "
-            "*is_security_capable=False)"
-        ),
-        "endpoints": {
-            "5": {
-                "profile_id": 0x0104,
-                "device_type": "0x0202",
-                "in_clusters": [
-                    "0x0000",
-                    "0x0003",
-                    "0x0004",
-                    "0x0005",
-                    "0x0102",
-                    "0x0b05",
-                ],
-                "out_clusters": ["0x0019"],
-            },
-            "21": {
-                "profile_id": 0x0104,
-                "device_type": "0x0104",
-                "in_clusters": [
-                    "0x0000",
-                    "0x0003",
-                    "0x0b05",
-                    "0xff17",
-                ],
-                "out_clusters": [
-                    "0x0003",
-                    "0x0005",
-                    "0x0006",
-                    "0x0008",
-                    "0x0019",
-                    "0x0102",
-                ],
-            },
-        },
-        "manufacturer": "Schneider Electric",
-        "model": "1GANG/SHUTTER/1",
-        "class": "zigpy.device.Device",
-    }
-    assert_signature_matches_quirk(
-        zhaquirks.schneiderelectric.shutters.OneGangShutter1, signature
-    )
-
-
-async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk):
+async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_v2_quirk):
     """Asserts that the go_to_lift_percentage command inverts the percentage value."""
 
-    device = zigpy_device_from_quirk(
-        zhaquirks.schneiderelectric.shutters.OneGangShutter1
+    device = zigpy_device_from_v2_quirk(
+        manufacturer=SE_MANUF_NAME,
+        model="1GANG/SHUTTER/1",
+        endpoint_ids=[5, 21],
     )
     window_covering_cluster = device.endpoints[5].window_covering
 
@@ -95,11 +37,13 @@ async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk
         assert request_mock.call_args[0][3] == 42  # 100 - 58
 
 
-async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_quirk):
+async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_v2_quirk):
     """Asserts that unpatched ZCL commands keep working."""
 
-    device = zigpy_device_from_quirk(
-        zhaquirks.schneiderelectric.shutters.OneGangShutter1
+    device = zigpy_device_from_v2_quirk(
+        manufacturer=SE_MANUF_NAME,
+        model="1GANG/SHUTTER/1",
+        endpoint_ids=[5, 21],
     )
     window_covering_cluster = device.endpoints[5].window_covering
 
@@ -115,14 +59,16 @@ async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_quirk):
         )
 
 
-async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk):
+async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_v2_quirk):
     """Asserts that updates to the ``current_position_lift_percentage`` attribute.
 
     (e.g., by the device) invert the reported percentage value.
     """
 
-    device = zigpy_device_from_quirk(
-        zhaquirks.schneiderelectric.shutters.OneGangShutter1
+    device = zigpy_device_from_v2_quirk(
+        manufacturer=SE_MANUF_NAME,
+        model="1GANG/SHUTTER/1",
+        endpoint_ids=[5, 21],
     )
     window_covering_cluster = device.endpoints[5].window_covering
     cluster_listener = ClusterListener(window_covering_cluster)