[APP][Pro] AWTRIX - Connected Smart Clock for your Smart Home

[HOW-TO][Pro] Repairing Flows after migrating from AWTRIX Light to AWTRIX NG

Based on Martijn Poppen’s original Flow-repair script, I prepared a customized HomeyScript to make migration from AWTRIX Light to AWTRIX NG easier.

Martijn’s script is excellent when a device is removed and re-added while the replacement exposes the same Flow cards. In that situation, replacing the old device UUID with the new UUID is sufficient.

AWTRIX Light to AWTRIX NG is slightly different:

  1. The device UUID changes.
  2. The old AWTRIX Light Flow card IDs are not always available for AWTRIX NG.

For example, an old card may contain:

homey:device:<new-device-id>:notificationIcon

After migration, AWTRIX NG uses:

homey:device:<new-device-id>:notification

The message, icon, color, and duration arguments are compatible, but the Flow-card ID is different. Therefore, replacing only the device UUID still leaves the Flow card unavailable or displayed as card: null. This could be addressed in the app code, but changing or remapping existing Flow cards would require additional backwards-compatibility safeguards. It would also not automatically repair Flows that were already stored in the broken format. For users who have already migrated, the customized script performs both operations:

  • replaces old device UUID references in regular and Advanced Flows;
  • changes the legacy notificationIcon card to the NG-compatible notification card;
  • supports flows where the UUID was already replaced but the old card ID remains;
  • provides a soft-run mode so changes can be reviewed before applying them.

For an already completed UUID replacement, use the same UUID in both arrays and keep softRun = true first:

const oldIds = [
  'NEW_DEVICE_ID',
];

const newIds = [
  'NEW_DEVICE_ID',
];

const softRun = true;

The script does not send commands to the display. It only updates stored Flow definitions. Nevertheless, use it carefully:

  • verify the device IDs;
  • run a soft-run first;
  • review the output;
  • set softRun = false only after confirming the planned changes;
  • test one repaired Flow afterwards.

This is specific to the AWTRIX Light → AWTRIX NG migration. The UUID replacement logic is generic, but the notificationIcon → notification mapping should not be reused for unrelated apps.

An app-side compatibility change could theoretically keep the legacy card available for NG devices, but that would not automatically rewrite already stored Flow JSON. The one-time migration script is therefore useful for repairing existing Flows without recreating them manually.

The customized script is included below.

The original Flow-repair approach is documented by Martijn in the Homey Community thread.

// HomeyScript: AWTRIX 3 -> AWTRIX NG Flow migration



const oldIds = \[

  '37614016-25f4-48e6-95a7-d9221361d7fa',

\];



const newIds = \[

  '1cca7b6e-f9c9-493e-827e-879afd423db0',

\];



// true = allow oldId === newId for auditing already-replaced UUIDs

const allowSameId = true;



// true = preview only; false = write changes

const softRun = true;



const detailedLog = true;



// AWTRIX 3 legacy card -> shared AWTRIX/NG card

const ACTION_CARD_MAP = new Map(\[

  \['notificationIcon', 'notification'\],

\]);



if (oldIds.length !== newIds.length || oldIds.length === 0) {

  throw new Error('oldIds and newIds must have the same non-zero length.');

}



const migrations = oldIds.map((oldId, index) => ({

  oldId,

  newId: newIds\[index\],

}));



for (const { oldId, newId } of migrations) {

  if (

    !oldId

    || !newId

    || (oldId === newId && !allowSameId)

  ) {

    throw new Error(\`Invalid migration pair: ${oldId} -> ${newId}\`);

  }

}



function deepReplace(value, from, to) {

  if (typeof value === 'string') {

    const parts = value.split(from);



    return {

      value: parts.length > 1 ? parts.join(to) : value,

      count: parts.length - 1,

    };

  }



  if (Array.isArray(value)) {

    let count = 0;



    const result = value.map((item) => {

      const replaced = deepReplace(item, from, to);

      count += replaced.count;

      return replaced.value;

    });



    return { value: result, count };

  }



  if (value && typeof value === 'object') {

    let count = 0;

    const result = {};



    for (const \[key, item\] of Object.entries(value)) {

      const replaced = deepReplace(item, from, to);

      result\[key\] = replaced.value;

      count += replaced.count;

    }



    return { value: result, count };

  }



  return { value, count: 0 };

}



// Supports normal Advanced Flow cards and the diagnostic shape:

// { card: null, data: { ... } }

function storedCard(card) {

  return card?.data && typeof card.data === 'object'

    ? card.data

    : card;

}



function belongsToDevice(card, deviceId) {

  const data = storedCard(card);

  const deviceUri = \`homey:device:${deviceId}\`;



  return \[

    data?.ownerUri,

    data?.uri,

    data?.id,

  \].some((value) => (

    typeof value === 'string'

    && (

      value === deviceUri

      || value.startsWith(\`${deviceUri}:\`)

    )

  ));

}



function migrateActionCardId(card, newDeviceId) {

  const data = storedCard(card);



  if (

    !data

    || typeof data.id !== 'string'

    || !belongsToDevice(card, newDeviceId)

  ) {

    return null;

  }



  for (const \[oldCardId, newCardId\] of ACTION_CARD_MAP) {

    // Regular Flow card: id = notificationIcon

    if (data.id === oldCardId) {

      data.id = newCardId;

      return { oldCardId, newCardId };

    }



    // Advanced Flow card:

    // id = homey:device:<uuid>:notificationIcon

    const suffix = \`:${oldCardId}\`;



    if (data.id.endsWith(suffix)) {

      data.id = \`${data.id.slice(0, -suffix.length)}:${newCardId}\`;

      return { oldCardId, newCardId };

    }

  }



  return null;

}



function migrateCard(card, location, details) {

  let updated = card;

  let changed = false;

  let uuidReplacements = 0;



  for (const migration of migrations) {

    // Same-ID mode is an audit/card-repair mode.

    // Do not count same-ID strings as replacements.

    const replaced = migration.oldId === migration.newId

      ? { value: updated, count: 0 }

      : deepReplace(

        updated,

        migration.oldId,

        migration.newId,

      );



    updated = replaced.value;



    if (replaced.count > 0) {

      changed = true;

      uuidReplacements += replaced.count;



      if (detailedLog) {

        details.push(

          \`${location}: UUID ${migration.oldId} -> \`

          + \`${migration.newId} (${replaced.count})\`,

        );

      }

    }



    // This also runs when UUID replacement already happened earlier.

    const cardIdChange = migrateActionCardId(

      updated,

      migration.newId,

    );



    if (cardIdChange) {

      changed = true;



      if (detailedLog) {

        details.push(

          \`${location}: card ${cardIdChange.oldCardId} -> \`

          + \`${cardIdChange.newCardId}\`,

        );

      }

    }

  }



  return {

    value: updated,

    changed,

    uuidReplacements,

  };

}



function migrateRegularFlow(flow) {

  const updated = JSON.parse(JSON.stringify(flow));

  const details = \[\];

  let changed = false;

  let uuidReplacements = 0;



  if (updated.trigger) {

    const result = migrateCard(

      updated.trigger,

      'trigger',

      details,

    );



    updated.trigger = result.value;

    changed ||= result.changed;

    uuidReplacements += result.uuidReplacements;

  }



  for (const section of \['conditions', 'actions'\]) {

    if (!Array.isArray(updated\[section\])) continue;



    updated\[section\] = updated\[section\].map((card, index) => {

      const result = migrateCard(

        card,

        \`${section}\[${index}\]\`,

        details,

      );



      changed ||= result.changed;

      uuidReplacements += result.uuidReplacements;



      return result.value;

    });

  }



  return {

    updated,

    changed,

    uuidReplacements,

    details,

  };

}



function migrateAdvancedFlow(flow) {

  const updated = JSON.parse(JSON.stringify(flow));

  const details = \[\];

  let changed = false;

  let uuidReplacements = 0;



  for (const \[key, card\] of Object.entries(updated.cards || {})) {

    const result = migrateCard(

      card,

      \`cards.${key}\`,

      details,

    );



    updated.cards\[key\] = result.value;

    changed ||= result.changed;

    uuidReplacements += result.uuidReplacements;

  }



  return {

    updated,

    changed,

    uuidReplacements,

    details,

  };

}



async function main() {

  console.log('======================================');

  console.log('AWTRIX Flow migration');

  console.log(\`Soft run: ${softRun}\`);

  console.log(\`Allow same ID: ${allowSameId}\`);

  console.log('Card mapping: notificationIcon -> notification');

  console.log('======================================');



  let regularPlanned = 0;

  let advancedPlanned = 0;

  let successfulWrites = 0;

  let failedWrites = 0;



  // Regular Flows

  const flows = Object.values(

    await Homey.flow.getFlows(),

  );



  for (const flow of flows) {

    const result = migrateRegularFlow(flow);



    if (!result.changed) continue;



    regularPlanned += 1;



    console.log(

      \`\[regular\] ${flow.name || flow.id}: planned\`,

    );



    if (result.uuidReplacements > 0) {

      console.log(

        \`  UUID replacements: ${result.uuidReplacements}\`,

      );

    }



    if (detailedLog) {

      result.details.forEach((line) => {

        console.log(\`  ${line}\`);

      });

    }



    if (!softRun) {

      try {

        // Send only mutable regular-Flow sections.

        const partial = {

          id: result.updated.id,

          trigger: result.updated.trigger,

          conditions: result.updated.conditions,

          actions: result.updated.actions,

        };



        await Homey.flow.updateFlow({

          id: flow.id,

          flow: partial,

        });



        successfulWrites += 1;

        console.log('  Written successfully');

      } catch (err) {

        failedWrites += 1;



        console.error(

          \`\[regular\] FAILED ${flow.name || flow.id}:\`,

          err?.message || err,

        );

      }

    }

  }



  // Advanced Flows

  const advancedFlows = Object.values(

    await Homey.flow.getAdvancedFlows(),

  );



  for (const flow of advancedFlows) {

    const result = migrateAdvancedFlow(flow);



    if (!result.changed) continue;



    advancedPlanned += 1;



    console.log(

      \`\[advanced\] ${flow.name || flow.id}: planned\`,

    );



    if (result.uuidReplacements > 0) {

      console.log(

        \`  UUID replacements: ${result.uuidReplacements}\`,

      );

    }



    if (detailedLog) {

      result.details.forEach((line) => {

        console.log(\`  ${line}\`);

      });

    }



    if (!softRun) {

      try {

        // Preserve the complete Advanced Flow.

        await Homey.flow.updateAdvancedFlow({

          id: flow.id,

          advancedflow: result.updated,

        });



        successfulWrites += 1;

        console.log('  Written successfully');

      } catch (err) {

        failedWrites += 1;



        console.error(

          \`\[advanced\] FAILED ${flow.name || flow.id}:\`,

          err?.message || err,

        );

      }

    }

  }



  console.log('======================================');

  console.log('Summary');

  console.log(\`Regular flows planned: ${regularPlanned}\`);

  console.log(\`Advanced flows planned: ${advancedPlanned}\`);

  console.log(

    \`Successful writes: ${softRun ? 0 : successfulWrites}\`,

  );

  console.log(\`Failed writes: ${failedWrites}\`);

  console.log(\`Soft run: ${softRun}\`);

  console.log('======================================');

}



main().catch((err) => {

  console.error(

    'Migration failed:',

    err?.message || err,

  );

});

Output example :

[regular] Postbox movement closed doors: planned (4 UUID occurrence(s))

  actions[11]: UUID 37614016-25f4-48e6-95a7-d9221361d7fa -> 1cca7b6e-f9c9-493e-827e-879afd423db0 (2)

  actions[11]: card notificationIcon -> notification

  actions[12]: UUID 37614016-25f4-48e6-95a7-d9221361d7fa -> 1cca7b6e-f9c9-493e-827e-879afd423db0 (2)

  actions[12]: card notificationIcon -> notification

[regular] Pohyb v rozvodné skříni domu: planned (2 UUID occurrence(s))

  actions[5]: UUID 37614016-25f4-48e6-95a7-d9221361d7fa -> 1cca7b6e-f9c9-493e-827e-879afd423db0 (2)

  actions[5]: card notificationIcon -> notification

[advanced] Developer check - nr of install app - adv: planned (0 UUID occurrence(s))

  cards.fb2ebb67-c354-4a5e-9284-bf14163daab8: card notificationIcon -> notification

  cards.518ea370-18cf-41e5-8f29-612c330f52fb: card notificationIcon -> notification

[advanced] HaasSohn check prior to the non-working day adv: planned (0 UUID occurrence(s))

  cards.d888d61a-61ea-42ee-8604-861bc2ae8474: card notificationIcon -> notification

  cards.1f7cf32e-9e65-41c3-93a7-c47fc55ca96d: card notificationIcon -> notification

  cards.069f64fd-464c-4dbf-b625-f36ba74e34ea: card notificationIcon -> notification

  cards.edb4573a-9c4b-462c-a8eb-ef388fd4733f: card notificationIcon -> notification

[advanced] Weather alerts activated, storm - adv.flow : planned (0 UUID occurrence(s))

  cards.3c31bfff-8276-4f03-bdee-95cef174401b: card notificationIcon -> notification

[advanced] AWTrix light Simon room adv: planned (0 UUID occurrence(s))

  cards.64f371e9-af66-47ed-8f26-1322e323cca1: card notificationIcon -> notification

  cards.b8d3418d-9eba-43dd-aa1b-94eb95157488: card notificationIcon -> notification

  cards.952b3e49-0eda-4d48-9e83-11362eecb928: card notificationIcon -> notification

  cards.600ca871-7b3d-47ae-98b5-0abda2a46cb1: card notificationIcon -> notification

  cards.df780320-7d5a-4cdc-99ad-176d5e313a4e: card notificationIcon -> notification

  cards.04098c11-f475-4637-97c9-1697c37bc2a2: card notificationIcon -> notification

  cards.e939bcf3-9eab-4e0e-8483-8ba6f833a891: card notificationIcon -> notification

  cards.e03fb580-97cc-42c9-aacf-758b7fcce599: card notificationIcon -> notification

  cards.160626c8-b0f8-4616-851b-926595db8b15: card notificationIcon -> notification

  cards.3b7f2b5a-3387-4fed-9b01-4cd7f948b60c: card notificationIcon -> notification

  cards.0b574371-4df0-4f11-b3b8-7ebd99e33acc: card notificationIcon -> notification

  cards.3121aca1-e3e4-407c-a0ec-ada139d269a8: card notificationIcon -> notification

  cards.1e2f4abd-18a7-400b-b9b2-5493fb4e08f8: card notificationIcon -> notification

  cards.f20517c5-05ab-4446-ae8e-3ec8badf0090: card notificationIcon -> notification

  cards.901e08fa-81fd-46ab-957f-eb81e5e95667: card notificationIcon -> notification

  cards.59ee56e2-a6c9-48f1-8f26-2edaa7844013: card notificationIcon -> notification

  cards.ebb212e2-7a65-4d8a-8a32-b364b45540c8: card notificationIcon -> notification

  cards.cd74ef2f-66ae-4397-ad39-6acd1c15823f: card notificationIcon -> notification

  cards.a00b0b28-45db-4549-a81a-3728fe24c123: card notificationIcon -> notification

[advanced] AWTrix notifications: planned (0 UUID occurrence(s))

  cards.b6fc60f1-388c-4bcc-a09e-34f9487d7a8d: card notificationIcon -> notification

  cards.3df5f31a-7884-4a92-b570-577f1849f4e4: card notificationIcon -> notification

  cards.847e03b9-a1e5-43ad-a30b-92dfe184a643: card notificationIcon -> notification

  cards.0ad352f8-e537-407a-836d-e7028111d6c0: card notificationIcon -> notification

[advanced] Doorbell rang - adv (nonAthom): planned (0 UUID occurrence(s))

  cards.d1adc972-73cc-4ec5-8090-7cff77f56458: card notificationIcon -> notification

[advanced] MKubeska YouTube adv: planned (0 UUID occurrence(s))

  cards.9158f02e-183e-4ead-8dc3-81d39bdc0423: card notificationIcon -> notification

[advanced] HaasSohn checks adv (Advanced Scheduler): planned (0 UUID occurrence(s))

  cards.de70a46c-6ebd-4d66-a7e6-f4ca4e3e9b96: card notificationIcon -> notification

  cards.893a09c8-dea3-4271-b0a3-496f9dd1e836: card notificationIcon -> notification

  cards.6dd15f03-b5fd-492f-991c-30a28d29a9de: card notificationIcon -> notification

[advanced] Water pressure calculation adv: planned (0 UUID occurrence(s))

  cards.1f5a6548-53ec-49d9-85ab-c6c645bff37d: card notificationIcon -> notification

  cards.7fe20785-b3ed-4834-9d57-90d162e3caf4: card notificationIcon -> notification

  cards.d8855cd9-ce7c-4083-b05c-54a2a23af312: card notificationIcon -> notification

  cards.77de0cb7-969a-4cbf-ba24-dd3835c4fc23: card notificationIcon -> notification

  cards.c76e39b5-7e48-40b6-83bd-be9f14273242: card notificationIcon -> notification

  cards.a2603d68-a70e-4586-84d3-e65d9182513e: card notificationIcon -> notification

  cards.7e1fd0cf-72f7-4d09-981f-c0f9ecb90006: card notificationIcon -> notification

[advanced] Movement in front of house Eufy adv: planned (0 UUID occurrence(s))

  cards.e984d301-c74c-4dda-982c-06029e58ed8f: card notificationIcon -> notification

[advanced] Airplane tracker OpenSky, Flightradar adv: planned (0 UUID occurrence(s))

  cards.33120d80-8a4a-4201-826c-54f296d9fdff: card notificationIcon -> notification

[advanced] Pračka susička změna stavu adv: planned (0 UUID occurrence(s))

  cards.215f3404-e317-4543-8530-1466ed7c23eb: card notificationIcon -> notification

  cards.fc26b0f5-0323-47fe-915e-706ef9776efd: card notificationIcon -> notification

  cards.96b2d7aa-9989-4898-ac5e-d6f95dd8de67: card notificationIcon -> notification

  cards.db8440ed-3748-4f9c-8a97-a31d6f55b670: card notificationIcon -> notification

--- Summary ---

Regular flows planned: 2

Advanced flows planned: 12

Successfully written: 14

Failed writes: 0

Soft run: false