Author: James Mounsey-Moran

  • Managing Power BI Refresh Schedules at Scale with PowerShell

    Managing Power BI Refresh Schedules at Scale with PowerShell

    As part of my work on Prism and other projects, managing Power BI refresh schedules at scale across multiple Power BI workspaces became essential. Manually setting schedules wasn’t scalable, and as Prism grew into a leading solution, automation became a necessity.

    To solve this, I built a PowerShell script using the Power BI API. The script scans workspaces based on a defined description, applies a refresh schedule, and updates the dataset ID accordingly.

    This script has now been used many times saving hours of manual work and hopefully it can help you too!

    Why Automate?

    Managing refresh schedules across multiple Power BI workspaces can quickly become time-consuming and inconsistent. Automating this process allows for simple bulk management and consistency.

    How it works?

    The script:

    • Connects to the PowerBI service
    • Retrieves all workspaces or filters with a specific name
    • Searches for reports starting with “Aether” (modify as you need)
    • Updates the refresh schedule to run daily at 16:00 UTC (modify time as you need)

    https://github.com/AetherAdv/powerbi_powershell_updaterefresh

    # Connect to Power BI Service Account
    
    Connect-PowerBIServiceAccount
    
     
    
    # Retrieve all workspaces
    
    $workspaces = Get-PowerBIWorkspace -All
    
     
    
     
    
    # Retrieve target workspace
    
    #$workspaces = Get-PowerBIWorkspace -id "####"
    
     
    
    # Loop through each workspace
    
    foreach ($workspace in $workspaces) {
    
     
    
        # Get reports with names starting with "AETHER" in the current workspace
    
        $Reportlist = Get-PowerBIReport -WorkspaceId $workspace.Id | Where-Object { $_.Name -like 'AETHER*' }
    
     
    
        # Check if any matching reports exist
    
        if ($Reportlist) {
    
            foreach ($Report in $Reportlist) {
    
     
    
                # Ensure the report has an associated DatasetId (required for refresh scheduling)
    
                if ($Report.DatasetId) {
    
                    $ReportName = $Report.Name
    
                    $WorkspaceId = $workspace.Id
    
                    $ReportId = $Report.Id
    
                    $DatasetId = $Report.DatasetId
    
     
    
                    Write-Host "Updating refresh schedule for: $ReportName in Workspace: $WorkspaceId"
    
     
    
                    # Construct the API URL for updating the refresh schedule
    
                    $ApiUrl = "https://api.powerbi.com/v1.0/myorg/groups/$WorkspaceId/datasets/$DatasetId/refreshSchedule"
    
     
    
                    # Define the refresh schedule settings
    
                    $ApiRequestBody = @{
    
                        value = @{
    
                            days            = @("Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday")  # Runs daily
    
                            times           = @("16:00")  # Refresh time in UTC
    
                            notifyOption    = "MailOnFailure"  # Notify only on failure
    
                            localTimeZoneId = "UTC"  # Time zone for the refresh schedule
    
                            enabled         = $true  # Ensure the schedule is enabled
    
                        }
    
                    } | ConvertTo-Json -Depth 3  # Convert to JSON format
    
     
    
                    try {
    
                        # Send API request to update the refresh schedule
    
                        Invoke-PowerBIRestMethod -Url $ApiUrl -Method Patch -Body $ApiRequestBody -Verbose
    
                        Write-Host "Successfully updated data refresh schedule for: $ReportName" -ForegroundColor Green
    
                    } catch {
    
                        # Handle any errors during the API request
    
                        Write-Host "Failed to update refresh schedule for: $ReportName" -ForegroundColor Red
    
                        Write-Host "Error: $_" -ForegroundColor Yellow
    
                    }
    
                } else {
    
                    # Skip reports without a DatasetId (they cannot have a refresh schedule)
    
                    Write-Host "Skipping report '$ReportName' as it has no associated DatasetId." -ForegroundColor Yellow
    
                }
    
            }
    
        }
    
    }

  • Data Encryption – PowerBI Vigenere Square

    Data Encryption – PowerBI Vigenere Square

    Data Encryption – Last month my wife and I picked up a detective box called Woodlock University, in there is a puzzle to decrypt some text using a Vigenère Square. (play it if you haven’t its great)

    My thought, I can build that and use it for data encryption for PowerBI and share with others working in data.

    I found it useful compared to some other solutions as it isn’t just random but a more complex cipher. This means if we still need to create relationships between two tables or more with this text as the identifier it will still link up as expected but act as a secure cipher.

    So here it is!

    The Vigenère cipher is a method of encrypting alphabetic text using substitution. It works by using a cipher (a keyword or passphrase) to shift the letters of the plaintext. In my example I used the cipher as Aether, but better to have a longer cipher if possible and of course not something that can be guessed.

    Note: Whilst this is a quick and relatively easy cipher to implement. AES encryption is definitely what you should be using !


    Data Encryption Process


    If you write the Plaintext (the word to encrypt) and Cipher (the keyword) beneath each other, repeating the cipher as necessary.

    Example:


    Plaintext:
    “Hello World”
    Cipher:
    “AETHE RAETH”


    Use a Vigenère Square: The Vigenère square is a table where each row is a shifted alphabet (A-Z). The cipher letter determines which row to use, and the plaintext letter determines which column to use for encryption.

    Power BI Setup

    I’ve set the data encryption as a PowerBI function so it can be called from elsewhere. The cipher is then driven from a parameter called “cipher”

    1. Create a new parameter called Cipher – Choose any value you want to use as your cipher
    2. Create a new blank query and copy in the below
    3. Call the function linking to text you want to encrypt

    Also available at the Aether repo here:

    https://github.com/AetherAdv/powerbi_data_vigeneresquare

    //JamesMM Aether
    
    let
        CipherVig = (CodeWord as text) as text =>
        let
            CipherKey = cipher, // Define the cipher key using a parameter
    
            // Define the alphabet as a list of uppercase letters A to Z.
            Alphabet = List.Transform({0..25}, each Character.FromNumber(Character.ToNumber("A") + _)),
    
            // Generate the Vigenère Square (as a list of records).
            // Each record represents a row of the Vigenère square.
            // Record field names are the letters A to Z.
            VigenereSquare = List.Transform(
                {0..25}, // Iterate through the rows of the square.
                each Record.FromList(
                    List.Transform(
                        {0..25}, // Iterate through the columns of the square.
                        (offset) => Alphabet{Number.Mod(offset + _, 26)} // Calculate the shifted letter using mod.
                    ),
                    Alphabet // Use the alphabet as field names for the record.
                )
            ),
    
            // Clean the input CodeWord by removing spaces and converting to uppercase.
            CleanWord = Text.Upper(Text.Remove(CodeWord, {" "})),
            // Clean the CipherKey by removing spaces and converting to uppercase.
            CleanCipher = Text.Upper(Text.Remove(CipherKey, {" "})),
    
            // Calculate the length of the cleaned CodeWord.
            WordLength = Text.Length(CleanWord),
    
            // Repeat the cipher key to match or exceed the CodeWord length.
            // This ensures the key is long enough to encrypt the entire message.
            // Example: HELLOWORLD becomes CIPHERCIPHE
            RepeatedCipher = Text.Start(Text.Repeat(CleanCipher, Number.RoundUp(WordLength / Text.Length(CleanCipher))), WordLength),
    
            // Encrypt each character of the CodeWord.
            EncryptedList = List.Transform(
                {0..WordLength - 1}, // Iterate through each character of the CodeWord.
                (index) =>
                    let
                        // Get the current character from the CodeWord.
                        Char = Text.At(CleanWord, index),
                        // Get the corresponding character from the repeated cipher key.
                        KeyChar = Text.At(RepeatedCipher, index),
                        // Find the index of the CodeWord character in the alphabet.
                        CharIndex = List.PositionOf(Alphabet, Char),
                        // Find the index of the cipher key character in the alphabet.
                        KeyIndex = List.PositionOf(Alphabet, KeyChar),
                        // Perform the Vigenère encryption.
                        EncryptedChar =
                            if CharIndex = -1 then Char // If the CodeWord character is not in the alphabet, keep it as is.
                            else if KeyIndex = -1 then Char // If the cipher key character is not in the alphabet, keep the CodeWord character as is.
                            else try Text.From(Record.Field(VigenereSquare{KeyIndex}, Alphabet{CharIndex})) otherwise Char // Retrieve the encrypted character from the Vigenère square. If an error occurs, keep the original character.
                    in
                        EncryptedChar // Return the encrypted character.
            ),
    
            // Combine the list of encrypted characters into a single text string.
            EncryptedWord = Text.Combine(EncryptedList)
    
        in
            EncryptedWord // Return the encrypted word.
    in
        CipherVig
  • Prism Week – Day 1

    Prism Week – Day 1

    This week we are celebrating 5 years of Prism, I know right!

    5 Years of Prism



    When I first joined Trustmarque, I was that anomaly that was working on Microsoft licensing, but was super techy after my time in the NHS and my excel skills were never going to match (unofficial excel world champion) Chris Phillips back when he was still at Trustmarque.

    But I loved building and creating. Prism started out as with many things I was building out at that time, a PowerShell script except this one automated bringing data together. That was until I was introduced to PowerBI.

    Working alongside some fantastic colleagues old and new (Steven Davison, Sean Hannah), amazing customers such as Jonathan West at DHCW, Prism evolved into something similar to the platform we know today

    Fast Forward!


    Fast forward five years, and Prism has grown into a full-fledged product. I’m really grateful to Trustmarque (notably Charlotte Henigan, Wesley Worland, Simon Williams and Simon Leuty) for encouraging innovation and turning what started as a side project on the odd train ride here and there into an industry-leading solution, gaining recognition through awards and customers alike.


    As part of this week Damien Masterson (who became just as passionate about Prism as I am) and I will be sharing some of the findings and statistics from when we first deploy Prism into customers and of course how Prism can help with optimisation all the way through to innovation.

    Inactive Users



    So, lets start with this first one, an absolutely huge 89% of customers had over >500 inactive users but the thing is they didn’t realise either. That’s optimisation that could quickly translate to >£100,000 that could be turned into innovation, that could fund kickstarting the new Microsoft Copilot project or those security improvements you know need doing. Prism finds this information and finds it fast in a way that’s accessible, readable, then with the support of our teams at Trustmarque and Livingstone ensures that you realise that optimisation and turns it into value.