--- # Phase 1: Query Omada client table and find the Philips Hue hub by MAC OUI. # Phase 2: Probe the discovered IP via the Hue Bridge local API. # # Philips Hue / Signify known MAC OUI prefixes: # 00:17:88 — classic Hue Bridge and bulbs # EC:B5:FA — newer Hue Bridge v2 and Hue products # # Usage: # ansible-playbook playbooks/network/omada_find_hue_hub.yml --ask-vault-pass - name: Find and probe Philips Hue hub via Omada client table hosts: localhost connection: local gather_facts: false vars_files: - "../../group_vars/all.yml" - "../../group_vars/vault/all.yml" vars: omada_validate_certs: false omada_page_size: 200 # Signify / Philips Hue MAC OUI prefixes (lowercase, colon-separated) hue_mac_ouis: - "00:17:88" - "ec:b5:fa" hue_probe_validate_certs: false tasks: # ----------------------------------------------------------------------- # PHASE 1 — Omada token # ----------------------------------------------------------------------- - name: Request Omada access token (client credentials) ansible.builtin.uri: url: "{{ omada_base_url }}/openapi/authorize/token?grant_type=client_credentials" method: POST validate_certs: "{{ omada_validate_certs }}" headers: Content-Type: application/json body_format: json body: omadacId: "{{ omada_id }}" client_id: "{{ omada_client_id }}" client_secret: "{{ omada_client_secret }}" return_content: true status_code: 200 register: omada_token_response no_log: true failed_when: - omada_token_response.json is not defined - omada_token_response.json.errorCode | default(-1) != 0 - name: Save access token ansible.builtin.set_fact: omada_access_token: "{{ omada_token_response.json.result.accessToken }}" no_log: true # ----------------------------------------------------------------------- # PHASE 1 — Get site list # ----------------------------------------------------------------------- - name: Query Omada sites ansible.builtin.uri: url: "{{ omada_base_url }}/openapi/v1/{{ omada_id }}/sites?page=1&pageSize={{ omada_page_size }}" method: GET validate_certs: "{{ omada_validate_certs }}" headers: Content-Type: application/json Authorization: "AccessToken={{ omada_access_token }}" return_content: true status_code: 200 register: omada_sites_response no_log: true failed_when: - omada_sites_response.json is not defined - omada_sites_response.json.errorCode | default(-1) != 0 - name: Save site list ansible.builtin.set_fact: omada_sites: "{{ omada_sites_response.json.result.data | default([]) }}" # ----------------------------------------------------------------------- # PHASE 1 — Query clients per site # ----------------------------------------------------------------------- - name: Query all clients per site ansible.builtin.uri: url: "{{ omada_base_url }}/openapi/v1/{{ omada_id }}/sites/{{ item.siteId }}/clients?page=1&pageSize={{ omada_page_size }}" method: GET validate_certs: "{{ omada_validate_certs }}" headers: Content-Type: application/json Authorization: "AccessToken={{ omada_access_token }}" return_content: true status_code: 200 loop: "{{ omada_sites }}" loop_control: label: "{{ item.name | default(item.siteId) }}" register: omada_clients_by_site no_log: true failed_when: false # ----------------------------------------------------------------------- # PHASE 1 — Filter for Hue hub by OUI # ----------------------------------------------------------------------- - name: Collect all clients from all sites into flat list ansible.builtin.set_fact: all_clients: "{{ omada_clients_by_site.results | selectattr('json', 'defined') | selectattr('json.errorCode', 'equalto', 0) | map(attribute='json.result.data') | flatten }}" - name: Filter clients by Hue MAC OUI prefixes ansible.builtin.set_fact: hue_candidates: "{{ all_clients | selectattr('mac', 'defined') | selectattr('ip', 'defined') | selectattr('mac', 'search', hue_mac_ouis | join('|'), ignorecase=True) | list }}" - name: Report Omada client search results ansible.builtin.debug: msg: - "Total clients scanned: {{ all_clients | length }}" - "Hue hub candidates found: {{ hue_candidates | length }}" - "Candidates: {{ hue_candidates | map(attribute='mac') | list }}" - name: Abort with clear message if no Hue hub found ansible.builtin.fail: msg: > No Philips Hue hub found in Omada client table. Verify the hub is powered on and connected to a monitored VLAN. Expected MAC OUI prefixes: {{ hue_mac_ouis | join(', ') }}. when: hue_candidates | length == 0 - name: Save first Hue hub candidate IP and MAC ansible.builtin.set_fact: hue_ip: "{{ hue_candidates[0].ip }}" hue_mac: "{{ hue_candidates[0].mac }}" hue_hostname: "{{ hue_candidates[0].hostname | default('unknown') }}" - name: Report discovered Hue hub ansible.builtin.debug: msg: - "Hue hub MAC : {{ hue_mac }}" - "Hue hub IP : {{ hue_ip }}" - "Hue hub hostname: {{ hue_hostname }}" # ----------------------------------------------------------------------- # PHASE 2 — Probe Hue Bridge local API # ----------------------------------------------------------------------- - name: Probe Hue Bridge local discovery endpoint ansible.builtin.uri: url: "http://{{ hue_ip }}/api/config" method: GET validate_certs: "{{ hue_probe_validate_certs }}" return_content: true timeout: 5 status_code: 200 register: hue_config_response failed_when: false - name: Probe Hue Bridge HTTPS clip v2 endpoint (newer firmware) ansible.builtin.uri: url: "https://{{ hue_ip }}/clip/v2/resource/bridge" method: GET validate_certs: "{{ hue_probe_validate_certs }}" return_content: true timeout: 5 status_code: [200, 401, 403] register: hue_clipv2_response failed_when: false - name: Build Hue adoption readiness summary ansible.builtin.set_fact: hue_adoption_summary: mac: "{{ hue_mac }}" ip: "{{ hue_ip }}" hostname: "{{ hue_hostname }}" local_api_v1: reachable: "{{ hue_config_response.status | default('n/a') == 200 }}" http_status: "{{ hue_config_response.status | default('n/a') }}" bridge_id: "{{ hue_config_response.json.bridgeid | default('n/a') }}" model_id: "{{ hue_config_response.json.modelid | default('n/a') }}" sw_version: "{{ hue_config_response.json.swversion | default('n/a') }}" api_version: "{{ hue_config_response.json.apiversion | default('n/a') }}" name: "{{ hue_config_response.json.name | default('n/a') }}" local_api_v2_clip: reachable: "{{ hue_clipv2_response.status | default('n/a') in [200, 401, 403] }}" http_status: "{{ hue_clipv2_response.status | default('n/a') }}" note: >- {{ 'CLIP v2 available (needs app_key header for full access)' if hue_clipv2_response.status | default(0) in [401, 403] else 'CLIP v2 accessible' if hue_clipv2_response.status | default(0) == 200 else 'CLIP v2 not reachable' }} ansible_adoption: method: "ansible.builtin.uri (REST API — no SSH required)" auth_required: "Hue app_key token (generate by pressing bridge button + POST /api)" next_step: >- {{ 'Bridge is reachable. Press the physical button on the bridge, then run omada_adopt_hue_hub.yml to generate an app_key.' if (hue_config_response.status | default(0) == 200 or hue_clipv2_response.status | default(0) in [200, 401, 403]) else 'Bridge API not responding. Check firewall rules for VLAN ' ~ hue_hostname ~ ' → Ansible control node.' }} - name: Print Hue adoption readiness summary ansible.builtin.debug: var: hue_adoption_summary