@sdetweil Great, will hopefully get my mirror on the wall next week!#
Read the statement by Michael Teeuw here.
Posts
-
RE: My module stops MMM-Pages working
-
RE: My module stops MMM-Pages working
@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!
-
RE: My module stops MMM-Pages working
@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 yetgetDom() 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…
-
RE: My module stops MMM-Pages working
@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; } });
-
RE: My module stops MMM-Pages working
@sdetweil
I seem to have lost the connection to my remote pi so will have a look tomorrow when I get my hands on itI think it is the original pages, will have a look at the fork
-
My module stops MMM-Pages working
My MagicMirror was working well, MMM-CalendarExt3 taking up the most of the display and fixed with MMM-Pages.
The clock & weather at the top on page0 and some bus timetables on page1 and these switch around with the calendar being static.I was never happy with the way the bus times displayed and after lots of tweaking of the module I was using I decided to write my own to do what I wanted. It works and does what I want but MMM-Pages does nothing with my module active, all the modules are displayed all the time. If I disable my module, it all works as it should.
I hadn’t noticed until now because I was working on it with only my module in the config.
I know I have not posed any code or much detail yet but can if I have an idea of what would be useful. I can update here rather than paste pages of possible unrequired code now.
This is my first attempt at writing a module, or anything in JS! so it has been a slow process. If there is something basic that I have not done that MMM-Pages requires then that is a good start.
-
RE: MMM-CalendarExt3 and MMM-MonthlyCalendar blank
All good, works fine.
-
RE: MMM-CalendarExt3 and MMM-MonthlyCalendar blank
@sdetweil
So, I have tried the following, each on a freshlly flashed OS.1 - Copied
sudo apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" full-upgrade
directly from the raspberry.sh script, updated and then upgraded using the above and it worked.
2 - Modified the script as below and ran it locally so I can see the output in terminal during the upgrade part, and it worked, no errors durring the upgrade. And Magic Mirror Ran successfully.
if [ $upgrade -eq $true ]; then sudo apt-get install pv -y >>$logfile echo "apt-get upgrade started" >> $logfile sudo apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" full-upgrade | tee -a $logfile #upgrade_result=$(sudo apt-get -y -o Dpkg::Options::="--force-confdef" -o Dpkg::Options::="--force-confnew" full-upgrade | pv -l -p) #upgrade_result=$(sudo apt-get --assume-yes full-upgrade 2>&1 | pv -l -p) #upgrade_rc=$? #echo apt-get upgrade result ="rc=$upgrade_rc $upgrade_result" >> $logfile fi
3 - Copied your raspberry.sh script to the home directory unmodified and ran it locally and it also works. Log file still reads
apt-get upgrade result =rc=0
But it worked, is the what you would expect in the log?
4 - Run the script again directly using.
bash -c "$(curl -sL https://raw.githubusercontent.com/sdetweil/MagicMirror_scripts/v231/raspberry.sh)"
This works and the log file reads the same
apt-get upgrade result =rc=0
Running apg-get full-upgrade again show that there is nothing new to do.
Bottom line, it works!
Attempt no 2 running the modified script locally, the whole upgrade output went to the log file.
-
RE: MMM-CalendarExt3 and MMM-MonthlyCalendar blank
MagicMirror did run though…
-
RE: MMM-CalendarExt3 and MMM-MonthlyCalendar blank
It seemed to get through the upgrade stage very quickly and
apt-get upgrade started apt-get upgrade result =rc=0 Reading package lists... Building dependency tree...
in the log seems to show the upgrade didn’t work. I will run the script locally on the pi and send the output to the termnial to see what is happening but it won’t be tonight, I have to go out now.
Update tomorrow…