MagicMirror Forum
    • Recent
    • Tags
    • Unsolved
    • Solved
    • MagicMirror² Repository
    • Documentation
    • 3rd-Party-Modules
    • Donate
    • Discord
    • Register
    • Login
    A New Chapter for MagicMirror: The Community Takes the Lead
    Read the statement by Michael Teeuw here.

    My module stops MMM-Pages working

    Scheduled Pinned Locked Moved Unsolved Troubleshooting
    13 Posts 2 Posters 324 Views 2 Watching
    Loading More Posts
    • Oldest to Newest
    • Newest to Oldest
    • Most Votes
    Reply
    • Reply as topic
    Log in to reply
    This topic has been deleted. Only users with topic management privileges can see it.
    • S Offline
      sdetweil @Scott-M
      last edited by sdetweil

      @Scott-M Pages is done all in browser, so open the developers window, ctrl-shift-i, and select the console tab

      put pages in the filter field and see if there are any errors…

      are you using the original pages or my fork?

      Sam

      How to add modules

      learning how to use browser developers window for css changes

      S 1 Reply Last reply Reply Quote 0
      • S Offline
        Scott-M @sdetweil
        last edited by

        @sdetweil
        I seem to have lost the connection to my remote pi so will have a look tomorrow when I get my hands on it

        I think it is the original pages, will have a look at the fork

        S 1 Reply Last reply Reply Quote 0
        • S Offline
          sdetweil @Scott-M
          last edited by

          @Scott-M on which pages source no need to change , just asking for later debug purposes

          Sam

          How to add modules

          learning how to use browser developers window for css changes

          S 1 Reply Last reply Reply Quote 0
          • S Offline
            Scott-M @sdetweil
            last edited by Scott-M

            @sdetweil
            There are no errors or issues in the console for MMM-Pages but what I didn’t see when my module is active is

            [MMM-pages] received that all objects are created; will now hide things!
            

            Which is present in the console when I disable it. I notice in the Pages docs it says

            MMM-pages doesn't activate until we receive the DOM_OBJECTS_CREATED notification, as that notification ensures all modules have been loaded.
            

            That is obviously not happening when my module is active. it loads the data from the NextBus API asyncronously so I think updateDom() keeps running before the first ready DOM is returned. I manually notifyied MagicMirror that it is ready by adding this into socketNotificationRecieved and seems to work now.

            if (!this.hasSentDomCreated) {
                    this.hasSentDomCreated = true;
                    this.sendNotification("DOM_OBJECTS_CREATED");
                  }
            

            There is probably a better way to do this but seems to work.

            This is my first attempt at a module so I wasn’t expecting plain sailing but any feedback is always appreciated.

            MMM-TravelLine.js

            // LCARS Colours
            var busColours = [
              "#cc99ff", //african-violet
              "#ffaa90", //almond
              "#ffbbaa", //almond-creme
              "#5566ff", //blue
              "#8899ff", //bluey
              "#ff9966", //butterscotch
              "#ffaa00", //gold
              "#ff9900", //golden-orange
              "#666688", //gray
              "#999933", //green
              "#99ccff", //ice
              "#cc55ff", //lilac
              "#cccc66", //lima-bean
              "#cc5599", //magenta
              "#ff2200", //mars
              "#9966ff", //moonlit-violet
              "#ff8800", //orange
              "#ff8866", //peach
              "#cc4444", //red
              "#aaaaff", //sky
              "#f5f6fa", //space-white
              "#ffcc99", //sunflower
              "#ff5555", //tomato
              "#ddbbff", //violet-creme
              // Lower Decks
              "#ffeecc", //butter
              "#ff9911", //daybreak
              "#ffaa44", //harvestgold
              "#ffcc99", //honey
              "#ff4400", //october-sunset
              "#ff7700", //orange
              "#cc5500" //rich-pumpkin
            ]
            
            function newColour () {
                var randomNumber = Math.floor(Math.random() * busColours.length);
                return randomNumber;
            }
            
            const directionColours = {
              "Edinburgh, Bus Station": "#cc99ff",          // 
              "Galashiels Transport Interchange": "#ff7700" // blue
            };
            
            // fallback colour if direction not in map
            const defaultColour = "#cc9bcd";
            
            Module.register("MMM-TravelLine", {
              defaults: {
                apiBase: 'https://nextbus.mxdata.co.uk/nextbuses/1.0/1',
                username: "yourusername",
                password: "yourpassword",
                // stopIDs[[stop ID, Name for heading, target bus, Random colours or not]]
                stopIDs: [  ["36235972","Princes St West (PD)","34",true],       
                            ["36232486","The Mound (MD)","27",true]],  
                //targetBus: "X62",     // Service we’re interested in
                updateFrequency: 10,   // Refresh time in minutes
                maxResults: 10,
                debug: false,
              },
            
              getStyles() {
                return ["travelline.css"];
              },
            
              start() {   
                // some dummy values
                this.timesReturned = [
                  {aimed: "..." ,service: "Error", direction: "...", raw: ""},
                  {aimed: "..." ,service: "Error", direction: "...", raw: ""},
                  {aimed: "..." ,service: "Error", direction: "...", raw: ""}
                ];
            
                this.sendSocketNotification("FETCH_ROUTES", {
                  username:this.config.username,
                  password:this.config.password,
                  apiBase:this.config.apiBase,
                  stopIDs:this.config.stopIDs,
                  //targetBus:this.config.targetBus,
                  timestamp: new Date().toISOString(),    // Build a fresh ISO8601 timestamp
            
                });
            
                // Fetch new routes at the interval set in the config
                setInterval(() => this.sendSocketNotification("FETCH_ROUTES", {
                  username:this.config.username,
                  password:this.config.password,
                  apiBase:this.config.apiBase,
                  stopIDs:this.config.stopIDs,
                  //targetBus:this.config.targetBus,
                  timestamp: new Date().toISOString(),    // Build a fresh ISO8601 timestamp
                }), this.config.updateFrequency * 60 * 1000);
            
                // Refresh the display every 30 seconds
                setInterval(() => this.updateDom(), 30000);
              },
            
              notificationReceived(notification, payload) {
                if (notification === "UPDATED_ROUTES") {
                  this.timesReturned = payload;
                  //this.updateDom();
                }
              },
            
              socketNotificationReceived: function (notification, payload) {
                if (notification === "UPDATED_ROUTES") {
                  this.timesReturned = payload;
                  this.updateDom();
                  // Notify once that DOM objects exist
                  if (!this.hasSentDomCreated) {
                    this.hasSentDomCreated = true;
                    this.sendNotification("DOM_OBJECTS_CREATED");
                  }
                }
              },
            
              getDom() {
                const container = document.createElement('div');
                container.className = 'travelline-container';
                container.style.display = 'flex';           // show tables side-by-side
                container.style.gap = '20px';               // spacing between tables
            
                //console.log("timesReturned is: ",this.timesReturned)
            
                // show message until we have data
                if (!Array.isArray(this.timesReturned) || this.timesReturned.length === 0) {
                  container.innerHTML = "<div class='no-data'>Loading bus times…</div>";
                  return container;
                }
                  // loop through each stop’s departures
                  this.timesReturned.forEach((stopResults, stopIndex) => {
                    const resultsArray = Array.isArray(stopResults) ? stopResults : [];
            
                    // create a wrapper for this stop
                    const stopWrapper = document.createElement('div');
                    stopWrapper.className = 'stop-wrapper';
            
                    // optional: heading for the stop
                    const heading = document.createElement('h3');
                    heading.className = "travelline-heading";
                    heading.textContent = `${resultsArray[0].stopName}`;  //heading.textContent = `Stop ${stopIndex + 1}`;
                    //heading.style.textAlign = 'center';
                    stopWrapper.appendChild(heading);
                    //if (!(stopResults[stopIndex].direction == "No Departures")) {
            
                    // create table
                    const table = document.createElement('table');
                    table.className = 'travelline-table';
            
                    // limit results
                    const limitedResults = resultsArray.slice(0, this.config.maxResults);
            
                    limitedResults.forEach(item => {
                      if (!(item.direction == "No Departures")) {
                        const row = document.createElement('tr');
                        row.className = 'travelline-row';
            
                        if (item.randomColour){
                          const colourIndex = newColour();
                          row.style.color = busColours[colourIndex];
                        } else {
                          // Pick a fixed colour based on destination direction
                          const dir = item.direction.trim();
                          row.style.color = directionColours[dir] || defaultColour;
                        }
            
                        // minutes-to-departure
                        const minutesCell = document.createElement('td');
                        minutesCell.className = 'travelline-minutes';
                        const depTime = new Date(item.raw);
                        const diffMs = depTime - Date.now();
                        const diffMin = Math.max(0, Math.round(diffMs / 60000));
                        if (diffMin >= 60) {
                          const hours = Math.floor(diffMin / 60);
                          const mins  = diffMin % 60;
                          minutesCell.textContent = `(${hours}h ${mins}m)`;
                        } else {
                          minutesCell.textContent = `(${diffMin} m)`;
                        }
            
                        // aimed departure time
                        const timeCell = document.createElement('td');
                        timeCell.className = 'travelline-time';
                        timeCell.textContent = item.aimed;
            
                        // service
                        const serviceCell = document.createElement('td');
                        serviceCell.className = 'travelline-service';
                        serviceCell.textContent = item.service;
            
                        // direction
                        const directionCell = document.createElement('td');
                        directionCell.className = 'travelline-direction';
                        directionCell.textContent = "To " + item.direction.replace(/,/g, "").split(" ")[0];
            
                        // assemble row
                        row.appendChild(minutesCell);
                        row.appendChild(timeCell);
                        row.appendChild(serviceCell);
                        row.appendChild(directionCell);
                        table.appendChild(row);
                      } else {
            
                        const row = document.createElement('tr');
                        row.className = 'travelline-row';
            
                        //Set row to a random colour
                        const colourIndex = newColour();
                        row.style.color = busColours[colourIndex];
            
                        // direction - should display'No Departures for the '
                        const directionCell = document.createElement('td');
                        directionCell.className = 'travelline-direction';
                        directionCell.textContent = item.direction + " for the ";
            
                        // service
                        const serviceCell = document.createElement('td');
                        serviceCell.className = 'travelline-service';
                        serviceCell.textContent = item.service;
            
                        // assemble row
                        row.appendChild(directionCell);    
                        row.appendChild(serviceCell);
                        table.appendChild(row);          
                      }
                    }); //limited results end
            
                    stopWrapper.appendChild(table);
                    container.appendChild(stopWrapper);
                  
                  });  
                return container;
              }
            });
            

            node_helper.js

            const NodeHelper = require("node_helper");
            //const fetch = require("node-fetch");	// Ensure fetch is available
            const { URL } = require("url"); 		// Ensure URL is available
            const { parseStringPromise } = require("xml2js");
            
            module.exports = NodeHelper.create({
            	async socketNotificationReceived(notification, payload) {
            		
            		console.log(`There are ${payload.stopIDs.length} stop IDs in the array`);
            		if (notification === "FETCH_ROUTES") {
            			try {
            				const authHeader = "Basic " + Buffer.from(`${payload.username}:${payload.password}`).toString("base64");
            				const siriBodies = this.buildSiriBodyArray(payload.stopIDs, payload.username, payload.timestamp);
            
                			// store results for all stops
            				const allResults = [];
            
                			// loop over each stop body
            				for (const [index, siriBody] of siriBodies.entries()) {
            					//console.log("siriBody is: ", siriBody[0]);
            					try {
            						const response = await fetch(payload.apiBase, {
            							method: "POST",
            							headers: {
            								"Content-Type": "application/xml",
            								"Authorization": authHeader
            							},
            							body: siriBody
            						});
            						if (!response.ok) {
            							console.error(`Fetch failed for stop ${payload.stopIDs[index][0]}: ${response.status}`);
            							continue;
            						}
            						const xmlText = await response.text();
            						const result = await parseStringPromise(xmlText, { explicitArray: false });
            
                    				// extract departures for this stop. stopIDs [stopID, StopName, target bus, randomColours]
            						let times = this.getTimes(result, payload.stopIDs[index]);
            						console.log("times is", times)
            						if (times[0] === "No Departures"){
            							console.log("Times returned and no departures or target bus not found");
            							const theService = times[1]
            							const theStop = times[2];
            							times = [{
            								service: theService,
                							direction: 'No Departures',
                							aimed: 0,
                							raw: 0,
                							stopID: '',
                							stopName: theStop,
                							randomColour: true
                						}];
            						}
                    				allResults.push(times); // Removed spread operator to seperate the data from each stop
                    			} catch (err) {
                    				console.error(`Error fetching stop ${payload.stopIDs[index][0]}:`, err);
                    			}
                    		}
                			// finally send combined result
                    		//console.log(allResults);
                    		this.sendSocketNotification("UPDATED_ROUTES", allResults);
                    	} catch (error) {
                    		console.error("General fetch error:", error);
                    	}
                    }
                },
            
                getTimes: function(receivedData, stopID) {
                	const departures = receivedData?.Siri?.ServiceDelivery?.StopMonitoringDelivery?.MonitoredStopVisit;
                	if (!departures){
                		console.log("No departures found at this stop");
                		return ["No Departures",stopID[2], stopID[1]];	// return 'No Departures', bus name, stop name
                	}
            
                	const departuresArray = Array.isArray(departures) ? departures : [departures];
                	const myBus = departuresArray.filter(d => d.MonitoredVehicleJourney?.PublishedLineName === stopID[2]); //target);
                	if (myBus.length === 0) {
                		console.log("No target bus found at this stop");
                		return ["No Departures",stopID[2], stopID[1]];	// return 'No Departures', bus name, stop name
                		}
                	return myBus.map(d => {
                		const call = d.MonitoredVehicleJourney.MonitoredCall;   	
                		const rawTime = call.ExpectedDepartureTime || call.AimedDepartureTime;
                		const dateObj = new Date(rawTime);
                		const isValidDate = rawTime && !isNaN(dateObj.getTime());
              			const friendlyTime = isValidDate
                			? dateObj.toLocaleTimeString('en-GB', {
                				hour: '2-digit', 
                				minute: '2-digit' 
                			})
                			: "N/A";
            
                		return {
                			service: stopID[2],		// target bus,
                			direction: d.MonitoredVehicleJourney.DirectionName,
                			aimed: friendlyTime,
                			raw: rawTime,
                  			stopID: stopID[0],		// stopID
                  			stopName: stopID[1],	// Description or stop name for the table heading
                  			randomColour: stopID[3]	// Random colour
              			};
            		});
                },
            
                buildSiriBodyArray: function(stopIDs, username, timestamp){
                	if (stopIDs.length == 0) {
                		console.log("The stopID array is empty");
                		return undefined;
                	};
                	const siriBodys = [];
                	stopIDs.forEach((item, index) =>{
                		siriBodys.push(`<?xml version="1.0" encoding="UTF-8"?>
            				<Siri version="1.0" xmlns="http://www.siri.org.uk/">
             					<ServiceRequest>
                					<RequestTimestamp>${timestamp}</RequestTimestamp>
                					<RequestorRef>${username}</RequestorRef>
                					<StopMonitoringRequest version="1.0">
                  						<RequestTimestamp>${timestamp}</RequestTimestamp>
                  						<MessageIdentifier>msg-001</MessageIdentifier>
                  						<MonitoringRef>${item[0]}</MonitoringRef>
                					</StopMonitoringRequest>
              					</ServiceRequest>
                	</Siri>`);
                	});
                	return siriBodys;
                }
            
            });
            
            S 1 Reply Last reply Reply Quote 0
            • S Offline
              sdetweil @Scott-M
              last edited by sdetweil

              @Scott-M you dont tell MagicMirror you are ready, it tells you

              You wait for ALL_MODULES_STARTED
              You can start operations in start()
              Do whatever, but you can’t draw yet

              getDom() will be called for your content the first time

              You never SEND any of those notifications
              sendNotification is a broadcast to all other modules,
              MagicMirror doesn’t receive it

              https://github.com/sdetweil/SampleModule

              See my sample module

              Sam

              How to add modules

              learning how to use browser developers window for css changes

              S S 2 Replies Last reply Reply Quote 0
              • S Offline
                sdetweil @sdetweil
                last edited by

                The only notice you can provide is calling updateNotification to inform MagicMirror that your module has new content to display
                AND Only after ALL_MODULES_STARTED, and After the first getDom() call

                Sam

                How to add modules

                learning how to use browser developers window for css changes

                1 Reply Last reply Reply Quote 0
                • S Offline
                  Scott-M @sdetweil
                  last edited by

                  @sdetweil said in My module stops MMM-Pages working:

                  you dont tell MagicMirror you are ready, it tells you

                  You wait for ALL_MODULES_STARTED
                  You can start operations in start()
                  Do whatever, but you can’t draw yet

                  getDom() will be called for your content the first time

                  You never SEND any of those notifications

                  https://github.com/sdetweil/SampleModule

                  See my sample module

                  You are absolutely correct, I should have properly read about the life cylce of MagicMirror first.

                  I changed socketNotificationRecieved to

                    socketNotificationReceived: function (notification, payload) {
                      if (notification === "UPDATED_ROUTES") {
                        this.timesReturned = payload;
                        this.loaded = true;
                        this.updateDom();
                      }
                    },
                  

                  and added this in getDom()

                      // show message until we have data
                      if (!this.loaded) {
                        container.innerHTML = "<div class='no-data'>Loading bus times…</div>";
                        return container;
                      }
                  

                  I think the module made updateDom() call before returning a valid DOM the first time. Let me know what you think! This also works now…

                  S 1 Reply Last reply Reply Quote 0
                  • S Offline
                    sdetweil @Scott-M
                    last edited by sdetweil

                    @Scott-M not quite…

                    i avoid this state setting (this.loaded) by always waiting til ALL_MODULES_STARTED
                    before asking my helper to work, then I KNOW its good on socketNotificationReceived to use updateDom

                    notificationReceived: function(notification, payload, sender) {
                    		// once everybody is loaded up
                    		if(notification==="ALL_MODULES_STARTED"){
                    			// send our config to our node_helper
                    			this.sendSocketNotification("CONFIG",this.config)
                    		}
                    

                    note: you CAN still get getDom() called BEFORE your socketNotification has been called by your node_helper sending data
                    so you need to check for data and return an empty DIV… (getDom MUST return SOMETHING)

                    Sam

                    How to add modules

                    learning how to use browser developers window for css changes

                    S 1 Reply Last reply Reply Quote 0
                    • S Offline
                      Scott-M @sdetweil
                      last edited by

                      @sdetweil
                      I think I understand, am I getting closer with something like this:

                      Module.register("MMM-TravelLine", {
                        start() {
                          this.timesReturned = [];
                        },
                      
                        notificationReceived(notification, payload, sender) {
                          if (notification === "ALL_MODULES_STARTED") {
                            console.log("All modules started; fetching routes now");
                            this.sendSocketNotification("FETCH_ROUTES", {
                              username: this.config.username,
                              password: this.config.password,
                              apiBase: this.config.apiBase,
                              stopIDs: this.config.stopIDs,
                              timestamp: new Date().toISOString()
                            });
                      
                            // Schedule refresh
                            setInterval(() => {
                              this.sendSocketNotification("FETCH_ROUTES", {
                                username: this.config.username,
                                password: this.config.password,
                                apiBase: this.config.apiBase,
                                stopIDs: this.config.stopIDs,
                                timestamp: new Date().toISOString()
                              });
                            }, this.config.updateFrequency * 60 * 1000);
                          }
                        },
                      
                        socketNotificationReceived(notification, payload) {
                          if (notification === "UPDATED_ROUTES") {
                            this.timesReturned = payload;
                            this.updateDom();
                          }
                        },
                      
                        getDom() {
                          const container = document.createElement("div");
                          container.className = "travelline-container";
                      
                          if (!Array.isArray(this.timesReturned) || this.timesReturned.length === 0) {
                            container.innerHTML = "<div class='no-data'>Loading bus times…</div>";
                            return container;
                          }
                      
                          // ...builds table etc.......
                      
                          return container;
                        }
                      });
                      
                      

                      Again, I have lost my remote conection to the PI so I can’t try it until tomorrow!

                      S 1 Reply Last reply Reply Quote 0
                      • S Offline
                        sdetweil @Scott-M
                        last edited by sdetweil

                        @Scott-M said in My module stops MMM-Pages working:

                        yes, looks good…

                        container.className = “travelline-container”;

                        you don’t have to set a class on the wrapper/container,
                        as MagicMirror has set your module name as a class on the module entry

                        open the developers window, and examine the html layout for a module in the elements tab
                        see the second link in my signature below

                        Sam

                        How to add modules

                        learning how to use browser developers window for css changes

                        S 1 Reply Last reply Reply Quote 0
                        • 1
                        • 2
                        • 1 / 2
                        • First post
                          Last post
                        Enjoying MagicMirror? Please consider a donation!
                        MagicMirror created by Michael Teeuw.
                        Forum managed by Sam, technical setup by Karsten.
                        This forum is using NodeBB as its core | Contributors
                        Contact | Privacy Policy