Repository: tronikos/opower (language: python)

Issue:
Ambiguous: "Re-login to make sure code handles already logged in sessions."
In `demo.py`, the login is performed twice.

https://github.com/tronikos/opower/blob/1900209b9d862d8edb6c30d8a472153169525eda/src/demo.py#L86-L88

Is this to test that it remembers the existing login, or is the correct behaviour to discard cookies and force a refreshed login?

I spent time at the weekend working on #54 – [andylittle/opower-smud/pull/1](https://github.com/andylittle/opower-smud/pull/1/files#diff-60a9722f47991898dc4315ab012791aa0a5a1f39d927f70cbd7217f70c7a471eR92) – where I'm returning early if the cookies are already set.
New Provider: SMUD 
I have the login to the SMUD page working. After opening the link to energy usage, a series of redirects occur. By setting  allow_redirects=True, I am able to regex the token from the redirect history. Is this the correct way to handle getting the token from the redirects or am I missing a concept here?


Discussion / hints:

Fix patch:
diff --git a/.gitignore b/.gitignore
index b6e4761..5710f84 100644
--- a/.gitignore
+++ b/.gitignore
@@ -109,6 +109,7 @@ venv/
 ENV/
 env.bak/
 venv.bak/
+.env.secret
 
 # Spyder project settings
 .spyderproject
diff --git a/README.md b/README.md
index b08208c..bbf151a 100644
--- a/README.md
+++ b/README.md
@@ -28,6 +28,7 @@ Supported utilities (in alphabetical order):
 - Pacific Gas & Electric (PG&E)
 - Portland General Electric (PGE)
 - Puget Sound Energy (PSE)
+- Sacramento Municipal Utility District (SMUD)
 - Seattle City Light (SCL)
 
 ## Support a new utility
diff --git a/pyproject.toml b/pyproject.toml
index dd36407..c96a311 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
 [project]
 name = "opower"
-version = "0.3.1"
+version = "0.4.0"
 license = {text = "Apache-2.0"}
 authors = [
     { name="tronikos", email="tronikos@gmail.com" },
@@ -77,3 +77,9 @@ fixture-parentheses = false
 
 [tool.ruff.mccabe]
 max-complexity = 25
+
+[project.optional-dependencies]
+dev = [
+    "pytest>=7,<8",
+    "python-dotenv>=1,<2",
+]
diff --git a/src/opower/utilities/smud.py b/src/opower/utilities/smud.py
new file mode 100644
index 0000000..89dff0c
--- /dev/null
+++ b/src/opower/utilities/smud.py
@@ -0,0 +1,270 @@
+"""Sacramento Municipal Utility District (SMUD)."""
+#
+# SMUD is a community owned, local monopoly power company in Sacramento, California.
+#
+# https://smud.org
+# https://en.wikipedia.org/wiki/Sacramento_Municipal_Utility_District
+# https://www.reddit.com/r/homeassistant/comments/10wnoox/smud_energy_smart_meter_integration/
+#
+# The web UI for power usage is at:
+# https://myaccount.smud.org/manage/opowerresidential/energyusage
+#
+# OKTA is an enterprise identity/SSO provider
+# https://www.okta.com/
+#
+# To filter FireFox network inspector when logging in:
+# `-regexp:woff|beacon|appdynamics|fonts|css|hotjar|usabilla|chat|ico|analytics|promo|nextprofilequestion|trackEvent
+#    -mime-type:image`
+#
+# Test with:
+# `python src/demo.py --utility smud --username mysmudloginemail@example.com --password "mypassword" -v`
+
+from html.parser import HTMLParser
+import logging
+from typing import Optional
+from urllib.parse import parse_qs
+
+from aiohttp import ClientResponse, ClientSession
+from aiohttp.client_exceptions import ClientResponseError
+from yarl import URL
+
+import opower
+
+from ..const import USER_AGENT
+from .base import UtilityBase
+
+_LOGGER = logging.getLogger(__file__)
+
+
+class SMUDLoginParser(HTMLParser):
+    """HTML parser to extract login verification token from SMUD Login page."""
+
+    def __init__(self) -> None:
+        """Initialize."""
+        super().__init__()
+        self.verification_token: Optional[str] = None
+
+    def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
+        """Try to extract the verification token from the login input."""
+        if tag == "input" and ("name", "__RequestVerificationToken") in attrs:
+            _, token = next(filter(lambda attr: attr[0] == "value", attrs))
+            _LOGGER.debug("SMUD self verify token: %s", token)
+            self.verification_token = token
+
+
+class SMUDOktaResponseSamlResponseValueParser(HTMLParser):
+    """HTML parser to extract SAMLResponse token from OKTA response for Opower SSO."""
+
+    # <input name="SAMLResponse" type="hidden" value="..."/>
+    def handle_starttag(self, tag: str, attrs: list[tuple[str, Optional[str]]]) -> None:
+        """Try to extract the SAMLResponse value."""
+        if tag == "input":
+            for name, value in attrs:
+                if name == "name" and value == "SAMLResponse":
+                    self.saml_response = attrs[2][1]
+
+
+class SMUD(UtilityBase):
+    """Sacramento Municipal Utility District (SMUD)."""
+
+    @staticmethod
+    def name() -> str:
+        """Distinct recognizable name of the utility."""
+        return "Sacramento Municipal Utility District (SMUD)"
+
+    @staticmethod
+    def subdomain() -> str:
+        """Return the opower.com subdomain for this utility."""
+        return "smud"
+
+    @staticmethod
+    def timezone() -> str:
+        """Return the timezone."""
+        return "America/Los_Angeles"
+
+    @staticmethod
+    async def async_login(
+        session: ClientSession,
+        username: str,
+        password: str,
+        optional_mfa_secret: Optional[str],  # Not used by SMUD.
+    ) -> None:
+        """Login to the utility website and authorize opower."""
+        # If we already have a cookie, return early if it is valid.
+        if (
+            len(session.cookie_jar.filter_cookies(URL("https://smud.opower.com/ei")))
+            > 0
+        ):
+            try:
+                async with session.get(
+                    "https://smud.opower.com/ei/edge/apis/multi-account-v1/cws/smud/customers",
+                    headers={"User-Agent": USER_AGENT},
+                    raise_for_status=True,
+                ):
+                    return
+            except ClientResponseError:
+                pass
+
+        smud_login_page_url = "https://myaccount.smud.org/"
+
+        _LOGGER.debug("Fetching SMUD login page: %s", smud_login_page_url)
+
+        myaccount_response = await session.get(
+            smud_login_page_url,
+            headers={"User-Agent": USER_AGENT},
+            raise_for_status=True,
+        )
+
+        await SMUD.log_response(myaccount_response, session)
+
+        # Parse the verification token which will be used during login.
+        # NB: Although the response cookies contain a `__RequestVerificationToken`, it does not match
+        # the one contained in the HTML and results in a 500 server error from SMUD during login.
+        login_parser = SMUDLoginParser()
+        login_parser.feed(await myaccount_response.text())
+        requestVerificationToken = login_parser.verification_token
+
+        _LOGGER.debug("Performing SMUD login to: %s", smud_login_page_url)
+
+        # Do the SMUD login which will set cookies in the session.
+        login_response = await session.post(
+            smud_login_page_url,
+            data={
+                "Lang": "en",
+                "UserID": username,
+                "Password": password,
+                "__RequestVerificationToken": requestVerificationToken,
+            },
+            headers={"User-Agent": USER_AGENT},
+            raise_for_status=True,
+        )
+
+        await SMUD.log_response(login_response, session)
+
+        login_response_body = await login_response.text()
+        if "could not be authenticated" in login_response_body:
+            raise opower.InvalidAuth
+
+        smud_energyusage_page_url = (
+            "https://myaccount.smud.org/manage/opowerresidential/energyusage"
+        )
+
+        _LOGGER.debug("Opening SMUD energy usage page: %s", smud_energyusage_page_url)
+
+        # Visit the Energy Usage page
+        energyusage_response = await session.get(
+            smud_energyusage_page_url,
+            headers={"User-Agent": USER_AGENT},
+            raise_for_status=True,
+        )
+
+        await SMUD.log_response(energyusage_response, session)
+
+        okta_login_2_url = SMUD.get_okta_url_from_response_redirect(
+            energyusage_response
+        )
+
+        _LOGGER.debug("Fetching second OKTA login page: %s", okta_login_2_url)
+
+        smud_okta_response = await session.get(
+            okta_login_2_url,
+            headers={"User-Agent": USER_AGENT},
+            raise_for_status=True,
+        )
+
+        await SMUD.log_response(smud_okta_response, session)
+
+        parser = SMUDOktaResponseSamlResponseValueParser()
+        parser.feed(await smud_okta_response.text())
+        saml_response = parser.saml_response
+        assert saml_response
+
+        _LOGGER.debug(
+            "Parsed SAMLResponse: %s...%s (%d characters)",
+            saml_response[0:5],
+            saml_response[-5:],
+            len(saml_response),
+        )
+
+        # This step is done in the web browser but doesn't seem to matter here.
+        #
+        # smud_ssotransition_url = "https://myaccount.smud.org/signin/ssotransition"
+        #
+        # _LOGGER.debug("Fetching SMUD ssotransition page: %s", smud_ssotransition_url)
+        # smud_ssotransition_response = await session.get(
+        #     smud_ssotransition_url,
+        #     headers={"User-Agent": USER_AGENT},
+        #     raise_for_status=True,
+        # )
+        #
+        # SMUD.print_redirects_cookies_response(smud_ssotransition_response, session)
+
+        opower_sso_url = "https://sso.opower.com/sp/ACS.saml2"
+
+        _LOGGER.debug("POSTing opower sso page with SAMLResponse: %s", opower_sso_url)
+
+        opower_sso_response = await session.post(
+            opower_sso_url,
+            data={
+                "SAMLResponse": saml_response,
+                "RelayState": "https://smud.opower.com/ei/app/myEnergyUse",
+            },
+            headers={"User-Agent": USER_AGENT},
+            raise_for_status=True,
+        )
+
+        await SMUD.log_response(opower_sso_response, session)
+
+        return
+
+    @classmethod
+    def get_okta_url_from_response_redirect(
+        cls, energyusage_response: ClientResponse
+    ) -> str:
+        """Get the OKTA URL to open next from the last redirect of the previous response."""
+        # https://smud.okta.com/login/sessionCookieRedirect
+        #   ?token=20111...6QJMn
+        #   &redirectUrl=https://smud.okta.com/app/sacramentomunicipalutilitydistrict_opower_1/exk2i...cF0x7/sso/saml
+        #       ?RelayState=https://smud.opower.com/ei/app/myEnergyUse
+        energyUsageResponseRedirectedFinalUrl = energyusage_response.history[-1].url
+
+        query_parts = parse_qs(energyUsageResponseRedirectedFinalUrl.query_string)
+
+        return query_parts["redirectUrl"][0]
+
+    # Store cookies so we can log what is new after each request.
+    cookies: dict[str, list[str]] = {}
+
+    @staticmethod
+    async def log_response(response: ClientResponse, session: ClientSession) -> None:
+        """Log any redirects and new cookies. Log full HTML when DEBUG_LOG_RESPONSE is set."""
+        host = response.host  # Is this the request URL or the final redirected url?
+
+        redirects = [r.url for r in response.history]
+        if len(redirects) > 1:
+            _LOGGER.debug("Performed %d redirects", len(redirects) - 1)
+            for redirect in redirects[1:]:
+                _LOGGER.debug("-> %s", redirect.__str__())
+
+        if len(session.cookie_jar.filter_cookies(response.url)) > 0:
+            response_cookie_names = list(
+                session.cookie_jar.filter_cookies(response.url).keys()
+            )
+            last_cookie_names = SMUD.cookies.get(host, [])
+            response_new_cookie_names = set(response_cookie_names) - set(
+                last_cookie_names
+            )
+
+            if len(response_new_cookie_names) > 0:
+                _LOGGER.debug(
+                    "Set new cookies: `%s`", "`, `".join(response_new_cookie_names)
+                )
+
+                SMUD.cookies[host] = last_cookie_names + response_cookie_names
+
+        if hasattr("opower", "DEBUG_LOG_RESPONSE") and getattr(
+            "opower", "DEBUG_LOG_RESPONSE"
+        ):
+            response_html = await response.text()
+            _LOGGER.debug("Response %s:", response.url)
+            _LOGGER.debug(response_html)


Test patch:
diff --git a/tests/opower/utilities/smud/myAccountResponse.html b/tests/opower/utilities/smud/myAccountResponse.html
new file mode 100644
index 0000000..b30a384
--- /dev/null
+++ b/tests/opower/utilities/smud/myAccountResponse.html
@@ -0,0 +1,515 @@
+
+
+
+<!DOCTYPE html>
+<html lang="en" class="chrome">
+
+<head>
+
+    <meta http-equiv="X-UA-Compatible" content="IE=edge">
+
+    <meta charset="utf-8" />
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <meta name="format-detection" content="telephone=no">
+
+    <script>
+    window["adrum-app-key"] = "AD-AAB-AAC-MEH";
+    (function (config) { (function (xd) { xd.enable = true; })(config.xd || (config.xd = {})); })(window['adrum-config'] || (window['adrum-config'] = {}));
+    window["adrum-start-time"] = new Date().getTime();
+</script>
+<script src="https://cdn.appdynamics.com/adrum/adrum-latest.js"></script>
+
+    <title>My Account | smud.org</title>
+
+    <link href="https://fonts.googleapis.com/css?family=Lato:400,700|Zilla+Slab:400,700&display=swap" rel="stylesheet">
+
+    <link href="/Content/css-theme?v=qGyaYKNG0WwUnRokx2gIKto5iyhTf6KldtwFghrC1iE1" rel="stylesheet"/>
+
+    <link href="/Content/css-app?v=ybZrOpNrQ6_hy4fZL-BosIj1Euymlr5TDmi6PDYgu2U1" rel="stylesheet"/>
+
+
+<link href="/Content/css-signin-controller?v=tznLQtfwR3tZMQl6BlysEcsOSgPCw2QwEcA3NZT1mg81" rel="stylesheet"/>
+</head>
+
+<body class="body CMS ab-xs lang-en signin-index"
+      data-area=""
+      data-controller="signin"
+      data-action="index"
+      data-sid="c4ldp1xslj2h0vaxrj2onfxa"
+      data-at=none
+      data-un=""
+      data-id=""
+      data-an=""
+      data-lang="en"
+       data-browser-name="Chrome" data-browser-version="115" data-is-browser-supported="true" data-has-user-acknowledged-lack-of-support="false">
+
+    <div id="page-container" class="d-flex flex-column">
+
+
+        <div id="header-container" class="container-fluid bottom-shadow">
+            <a href="#main-container" class="skip-nav-hidden gtm-nav-link-skip-nav-hidden">Skip navigation</a>
+
+
+
+<header class="mt-4" >
+    <div id="logo-and-utility-nav-container">
+        <div class="">
+            <div class="row">
+                <div class="col-4">
+                    <a href="https://www.smud.org/" target="_blank"><img class="smud-logo" alt="SMUD Logo" src="/Images/smud-logo.svg" onerror="this.src = '/Images/smud-logo.svg'; this.onerror = null;" /></a>
+                </div>
+                <div id="utility-nav" class="col-7 ml-auto text-right form-control-static dropdown">
+                </div>
+            </div>
+        </div>
+    </div>
+
+    <div id="mobile-nav-container">
+        <div id="mobile-nav" class="d-block d-md-none">
+
+            <div class="row">
+                <div id="memento-user-name-mobile" class="col-6">
+
+                </div>
+                <div id="memento-account-number-mobile" class="col-6 text-right">
+                </div>
+            </div>
+
+        </div>
+    </div>
+
+</header>
+        </div>
+
+
+        <main id="main-container" class="container-fluid">
+            <article id="main-article">
+                                    <h1 id="page-title">My Account</h1>
+
+
+
+                <div id="page-content">
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+<div class="row">
+    <div class="col-12 col-md-6 d-flex">
+        <div class="card sign-in-card display-flex shadow-sm">
+            <div class="card-body p-4">
+                <h2>Sign in to My Account</h2>
+
+<form action="/?Length=0" id="sign-in-form" method="post" role="form"><input name="__RequestVerificationToken" type="hidden" value="O04jyV4GhU2Em2TpwzEmRjy75u01mS8TDd7LKz24xX9yQ1-zc0oXwlniJvfROS0EqmnpKlvf9jkpb43Z4nrQOFW8lm8B_umYf7D3YuN5fWQ1" /><input id="Lang" name="Lang" type="hidden" value="en" /><div class="form-group" id="userid-form-group"><label class="control-label" for="UserId">User ID</label><div class="form-group-content">                        <div class="control-help">You must verify your email before you can use it as a User ID.</div>
+                        <div class="col-lg-9 col-xl-6 pl-0">
+
+
+<input class="form-control has-initial-focus" data-val="true" data-val-length="User ID should be 8 to 241 characters" data-val-length-max="241" data-val-length-min="8" data-val-required="Please enter the User ID" id="UserId" name="UserId" placeholder="" type="text" value="" />
+                            <span class="field-validation-valid text-danger" data-valmsg-for="UserId" data-valmsg-replace="true"></span>
+                        </div>
+</div></div>                    <div class="has-initial-focus col-lg-9 col-xl-6 pl-0">
+                        <div class="form-group" id="password-form-group"><label class="control-label" for="Password">Password</label>
+<div class="input-group input-with-icon">
+<input class="form-control " data-val="true" data-val-length="Password should be 6 to 32 characters" data-val-length-max="32" data-val-length-min="6" data-val-required="Please enter the Password" id="Password" name="Password" type="password" value="" />    <div class="input-group-addon">
+        <a href="#" class="password-toggle" data-show-label="Show" data-hide-label="Hide">Show</a>
+    </div>
+</div>
+<span class="field-validation-valid text-danger" data-valmsg-for="Password" data-valmsg-replace="true"></span></div>
+                    </div>
+                    <div class="col-9 col-lg-5 pl-0">
+                        <button type="submit" class="btn btn-primary btn-block gtm-signin-btn-signin">Sign in</button>
+                    </div>
+                    <div class="mt-d">
+                        <p>
+                            <a href="/recoveruser" class="gtm-signin-link-forgotuserid">Forgot your User ID?</a>
+                        </p>
+                        <p>
+                            <a href="/recover/password" class="gtm-signin-link-forgotpassword">Forgot your password?</a>
+                        </p>
+                    </div>
+</form>            </div>
+        </div>
+    </div>
+
+        <div class="col-12 col-md-6 d-flex">
+            <div class="card sign-in-card display-flex shadow-sm">
+                <div class="card-body p-4">
+                    <h2>Register for My Account</h2>
+
+                    <p class="mb-b">
+                        With My Account you can:
+                    </p>
+
+                    <div class="row">
+                        <div class="col-lg-6">
+                            <div class="row mb-b">
+                                <div class="col-2"><svg class="icon icon-credit-card" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g>
+            <rect x="0" y="0" width="24" height="24"></rect>
+            <path d="M4,5 L20,5 C21.1045695,5 22,5.8954305 22,7 L22,17 C22,18.1045695 21.1045695,19 20,19 L4,19 C2.8954305,19 2,18.1045695 2,17 L2,7 C2,5.8954305 2.8954305,5 4,5 Z M4,8 L4,11 L20,11 L20,8 L4,8 Z" fill="currentColor"></path>
+        </g>
+    </g>
+</svg></div>
+                                <div class="col-10 icon-description">Pay your bill</div>
+                            </div>
+                            <div class="row mb-b">
+                                <div class="col-2"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-file" width="24px" height="24px" viewBox="0 0 18 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Sign-in-and-Reg" transform="translate(-774.000000, -389.000000)" fill="#A6978A" fill-rule="nonzero">
+            <g id="file-alt-regular" transform="translate(774.000000, 389.000000)">
+                <path d="M13.5,11.625 L13.5,12.9375 C13.5,13.246875 13.246875,13.5 12.9375,13.5 L5.0625,13.5 C4.753125,13.5 4.5,13.246875 4.5,12.9375 L4.5,11.625 C4.5,11.315625 4.753125,11.0625 5.0625,11.0625 L12.9375,11.0625 C13.246875,11.0625 13.5,11.315625 13.5,11.625 Z M12.9375,15 L5.0625,15 C4.753125,15 4.5,15.253125 4.5,15.5625 L4.5,16.875 C4.5,17.184375 4.753125,17.4375 5.0625,17.4375 L12.9375,17.4375 C13.246875,17.4375 13.5,17.184375 13.5,16.875 L13.5,15.5625 C13.5,15.253125 13.246875,15 12.9375,15 Z M18,6.1828125 L18,21.75 C18,22.9921875 16.9921875,24 15.75,24 L2.25,24 C1.0078125,24 0,22.9921875 0,21.75 L0,2.25 C0,1.0078125 1.0078125,0 2.25,0 L11.8171875,0 C12.4125,0 12.984375,0.2390625 13.40625,0.6609375 L17.3390625,4.59375 C17.7609375,5.0109375 18,5.5875 18,6.1828125 Z M12,2.4328125 L12,6 L15.5671875,6 L12,2.4328125 Z M15.75,21.75 L15.75,8.25 L10.875,8.25 C10.2515625,8.25 9.75,7.7484375 9.75,7.125 L9.75,2.25 L2.25,2.25 L2.25,21.75 L15.75,21.75 Z" id="Shape"></path>
+            </g>
+        </g>
+    </g>
+</svg></div>
+                                <div class="col-10 icon-description">Access your bill</div>
+                            </div>
+                            <div class="row mb-b">
+                                <div class="col-2"><svg class="icon icon-usage" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g>
+            <rect x="0" y="0" width="24" height="24"></rect>
+            <path d="M4,8 L8,8 L8,22 L4,22 L4,8 Z M16,12 L20,12 L20,22 L16,22 L16,12 Z M10,2 L14,2 L14,22 L10,22 L10,2 Z" fill="currentColor"></path>
+        </g>
+    </g>
+</svg></div>
+                                <div class="col-10 icon-description">View your energy usage</div>
+                            </div>
+                        </div>
+                        <div class="col-lg-6">
+                            <div class="row mb-b">
+                                <div class="col-2"><svg class="icon icon-home" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="home">
+            <rect x="0" y="0" width="24" height="24"></rect>
+            <polygon fill="currentColor" points="12 2 22 12 20 12 20 22 15 22 15 15 9 15 9 22 4 22 4 12 2 12"></polygon>
+        </g>
+    </g>
+</svg></div>
+                                <div class="col-10 icon-description">Find ways to save energy</div>
+                            </div>
+                            <div class="row mb-b">
+                                <div class="col-2"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-alerts" width="24px" height="24px" viewBox="0 0 15 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="Sign-in-and-Reg" transform="translate(-1065.000000, -388.000000)" fill="#A6978A" fill-rule="nonzero">
+            <g id="Group-3-Copy" transform="translate(1065.000000, 388.000000)">
+                <g id="Group-2">
+                    <g id="Group">
+                        <g id="mobile-alt-solid">
+                            <path d="M12.5538462,0 L2.21538462,0 C0.992307692,0 0,0.981971154 0,2.19230769 L0,21.1923077 C0,22.4026442 0.992307692,23.3846154 2.21538462,23.3846154 L12.5538462,23.3846154 C13.7769231,23.3846154 14.7692308,22.4026442 14.7692308,21.1923077 L14.7692308,2.19230769 C14.7692308,0.981971154 13.7769231,0 12.5538462,0 Z M7.38461538,21.9230769 C6.56769231,21.9230769 5.90769231,21.2699519 5.90769231,20.4615385 C5.90769231,19.653125 6.56769231,19 7.38461538,19 C8.20153846,19 8.86153846,19.653125 8.86153846,20.4615385 C8.86153846,21.2699519 8.20153846,21.9230769 7.38461538,21.9230769 Z M12.5538462,16.9903846 C12.5538462,17.2918269 12.3046154,17.5384615 12,17.5384615 L2.76923077,17.5384615 C2.46461538,17.5384615 2.21538462,17.2918269 2.21538462,16.9903846 L2.21538462,2.74038462 C2.21538462,2.43894231 2.46461538,2.19230769 2.76923077,2.19230769 L12,2.19230769 C12.3046154,2.19230769 12.5538462,2.43894231 12.5538462,2.74038462 L12.5538462,16.9903846 Z" id="Shape"></path>
+                        </g>
+                    </g>
+                </g>
+                <g id="info-solid" transform="translate(5.538462, 4.615385)">
+                    <path d="M0.384615385,8.41319531 L0.769230769,8.41319531 L0.769230769,5.54834315 L0.384615385,5.54834315 C0.172192308,5.54834315 0,5.37076983 0,5.15170853 L0,4.20432692 C0,3.98526562 0.172192308,3.80769231 0.384615385,3.80769231 L2.53846154,3.80769231 C2.75088462,3.80769231 2.92307692,3.98526562 2.92307692,4.20432692 L2.92307692,8.41319531 L3.30769231,8.41319531 C3.52011538,8.41319531 3.69230769,8.59076863 3.69230769,8.80982993 L3.69230769,9.75721154 C3.69230769,9.97627284 3.52011538,10.1538462 3.30769231,10.1538462 L0.384615385,10.1538462 C0.172192308,10.1538462 0,9.97627284 0,9.75721154 L0,8.80982993 C0,8.59076863 0.172192308,8.41319531 0.384615385,8.41319531 Z M1.84615385,0 C1.08144231,0 0.461538462,0.639275841 0.461538462,1.42788462 C0.461538462,2.21649339 1.08144231,2.85576923 1.84615385,2.85576923 C2.61086538,2.85576923 3.23076923,2.21649339 3.23076923,1.42788462 C3.23076923,0.639275841 2.61084615,0 1.84615385,0 Z" id="Shape"></path>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg></div>
+                                <div class="col-10 icon-description">Sign up for billing alerts</div>
+                            </div>
+                        </div>
+                    </div>
+
+                    <div class="btn-container col-9 col-lg-5 pl-0">
+                        <a href="/register" class="btn btn-outline-primary btn-block js-link-waiting gtm-signin-link-register">Register</a>
+                    </div>
+                </div>
+            </div>
+        </div>
+</div>
+                </div>
+            </article>
+        </main>
+
+        <div id="footer-container" class="mt-auto container-fluid p-0">
+            <div class="footer-wave mb-4"></div>
+            <div class="footer-bg pb-3">
+
+<footer class="container-fluid">
+    <div class="row">
+        <div class="col-12 mb-0">
+            <h3>QUICK LINKS</h3>
+        </div>
+        <div class="col-12 col-lg-3 pr-0">
+            <p class="mb-s">
+                    <a href="/manage/outage" class="gtm-footer-link-outage-center">Outage Center</a>
+            </p>
+            <p class="mb-s">
+                <a href="/manage/paymentarrangements" class="gtm-footer-link-payment-arrangements">Payment arrangements</a>
+            </p>
+            <p class="mb-s">
+                    <a href="/manage/rebate/rebateapplication" class="gtm-footer-link-rebates">Rebates</a>
+            </p>
+        </div>
+        <div class="col-12 col-lg-9">
+            <p class="mb-s">
+                <a href="https://www.smud.org/" class="gtm-footer-link-smudorg" target="_blank">SMUD.org</a>
+            </p>
+            <p class="mb-s">
+                <a href="https://www.smud.org/en/Corporate/About-us" class="gtm-footer-link-about-us" target="_blank">About us</a>
+            </p>
+            <p class="mb-s">
+                <a href="/manage/contactus" class="gtm-footer-link-contact-us">Contact us</a>
+            </p>
+            <p class="d-block d-lg-none mb-s">
+                <a href="#" class="survey-link gtm-footer-link-feedback">Give feedback</a>
+            </p>
+        </div>
+    </div>
+    <hr />
+    <div class="row">
+
+        <div class="col-lg-9 d-none d-lg-block">
+            <span class="smaller-font">
+                &copy; 2024 Sacramento Municipal Utility District
+                <svg class="footer-dot icon icon-dot" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <path d="M12.0026513,16 C9.79623255,16 8,14.2041794 8,12.0004278 C8,9.7946228 9.79623255,8 12.0026513,8 C14.2073596,8 16,9.7946228 16,12.0004278 C16,14.2041794 14.2073596,16 12.0026513,16" id="dot-path-1"></path>
+    </defs>
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g>
+            <rect fill-rule="nonzero" x="0" y="0" width="24" height="24"></rect>
+            <mask id="mask-2" fill="white">
+                <use xlink:href="#path-1"></use>
+            </mask>
+            <use fill="currentColor" xlink:href="#dot-path-1"></use>
+        </g>
+    </g>
+</svg>
+            </span>
+            <span class="smaller-font">
+                <a href="https://www.smud.org/privacy" class="gtm-footer-link-privacy-policy" target="_blank">Privacy Policy</a>
+                <svg class="footer-dot icon icon-dot" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <path d="M12.0026513,16 C9.79623255,16 8,14.2041794 8,12.0004278 C8,9.7946228 9.79623255,8 12.0026513,8 C14.2073596,8 16,9.7946228 16,12.0004278 C16,14.2041794 14.2073596,16 12.0026513,16" id="dot-path-1"></path>
+    </defs>
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g>
+            <rect fill-rule="nonzero" x="0" y="0" width="24" height="24"></rect>
+            <mask id="mask-2" fill="white">
+                <use xlink:href="#path-1"></use>
+            </mask>
+            <use fill="currentColor" xlink:href="#dot-path-1"></use>
+        </g>
+    </g>
+</svg>
+            </span>
+            <span class="smaller-font">
+                <a href="https://www.smud.org/en/Corporate/Privacy-policy/Terms-and-conditions" class="gtm-footer-link-terms-and-conditions">Terms and conditions</a>
+            </span>
+            <span class="smaller-font">
+<svg class="footer-dot icon icon-dot" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <path d="M12.0026513,16 C9.79623255,16 8,14.2041794 8,12.0004278 C8,9.7946228 9.79623255,8 12.0026513,8 C14.2073596,8 16,9.7946228 16,12.0004278 C16,14.2041794 14.2073596,16 12.0026513,16" id="dot-path-1"></path>
+    </defs>
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g>
+            <rect fill-rule="nonzero" x="0" y="0" width="24" height="24"></rect>
+            <mask id="mask-2" fill="white">
+                <use xlink:href="#path-1"></use>
+            </mask>
+            <use fill="currentColor" xlink:href="#dot-path-1"></use>
+        </g>
+    </g>
+</svg><a href="/nav/togglelanguage">En espa&#241;ol</a>            </span>
+        </div>
+
+        <div class="col-12 col-lg-3 text-left text-lg-right mb-c">
+            <a href="http://twitter.com/SMUDUpdates" target="_blank" class="footer-social-media-link gtm-footer-link-twitter"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-twitter-block" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <title id="TwitterTitle">Twitter</title>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <rect fill="#FFFFFF" x="0" y="0" width="24" height="24"></rect>
+    <path d="M19.066122,8.77067734 C19.076372,8.91351578 19.076372,9.05651955 19.076372,9.199358 C19.076372,13.5577491 15.7591806,18.5795786 9.69615144,18.5795786 C7.82817505,18.5795786 6.09311777,18.0384788 4.6334874,17.0994482 C4.89899498,17.1300329 5.15408725,17.1402828 5.42967948,17.1402828 C6.97081373,17.1402828 8.3896095,16.6198483 9.52256305,15.7317371 C8.07318266,15.7011524 6.85855991,14.7518719 6.44012923,13.44533 C6.64430224,13.4760799 6.84847525,13.4964146 7.06273291,13.4964146 C7.35882511,13.4964146 7.65475198,13.45558 7.93034421,13.3841607 C6.41979459,13.0779839 5.28667571,11.7511073 5.28667571,10.1486385 L5.28667571,10.1078039 C5.72560635,10.3526462 6.23595622,10.5058999 6.7768907,10.5262346 C5.88894476,9.93421551 5.30717568,8.92376577 5.30717568,7.78056223 C5.30717568,7.16820853 5.47051408,6.60677408 5.75619097,6.11675886 C7.37915975,8.11732371 9.81865524,9.42386565 12.5540776,9.56670409 C12.502993,9.3218618 12.4724084,9.06676953 12.4724084,8.81151194 C12.4724084,6.99462014 13.9421234,5.51465513 15.7692652,5.51465513 C16.7185457,5.51465513 17.5757417,5.91275117 18.1780108,6.55585482 C18.9231183,6.41285105 19.6376411,6.13725882 20.2704948,5.75966274 C20.0254872,6.52510488 19.5048873,7.16820853 18.8211144,7.57638922 C19.4845527,7.50497 20.1276564,7.32129695 20.7195101,7.06603936 C20.2704948,7.71939299 19.7092257,8.30116208 19.066122,8.77067734 M23.3387109,0 L0.6612891,0 C0.297580095,0 0,0.297580095 0,0.6612891 L0,23.3387109 C0,23.7024199 0.297580095,24 0.6612891,24 L23.3387109,24 C23.7024199,24 24,23.7024199 24,23.3387109 L24,0.6612891 C24,0.297580095 23.7024199,0 23.3387109,0" fill="#555"></path>
+  </g>
+</svg></a>
+            <a href="http://www.facebook.com/pages/SMUD/312101029809" target="_blank" class="footer-social-media-link gtm-footer-link-facebook"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-facebook-block" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <title id="FacebookTitle">Facebook</title>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <rect fill="#FFFFFF" x="0" y="0" width="24" height="24"></rect>
+    <g fill="#555">
+      <path d="M0.6612891,0 C0.297580095,0 0,0.297580095 0,0.6612891 L0,23.3387109 C0,23.7024199 0.297580095,24 0.6612891,24 L12.8521537,24 L12.8521537,14.6695414 L9.83783262,14.6695414 L9.83783262,11.09329 L12.8521537,11.09329 L12.8521537,8.27553713 C12.8521537,5.21393391 14.7227752,3.5476507 17.4542298,3.5476507 C18.762921,3.5476507 19.8869471,3.64585213 20.213128,3.68916657 L20.213128,6.88815259 L18.3188653,6.88815259 C16.8332794,6.88815259 16.5464452,7.59556661 16.5464452,8.62916147 L16.5464452,11.09329 L19.898685,11.09329 L19.4389238,14.6695414 L16.5464452,14.6695414 L16.5464452,24 L23.3387109,24 C23.7024199,24 24,23.7024199 24,23.3387109 L24,0.6612891 C24,0.297580095 23.7024199,0 23.3387109,0 L0.6612891,0 Z"></path>
+    </g>
+  </g>
+</svg></a>
+            <a href="https://www.youtube.com/user/MySMUD?reload=9" target="_blank" class="footer-social-media-link gtm-footer-link-youtube"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-youtube-block" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <title id="YoutubeTitle">YouTube</title>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <rect fill="#FFFFFF" x="0" y="0" width="24" height="24"></rect>
+    <path d="M18.1302326,13.0740162 L8.87912875,18.1031198 C8.04590449,18.557756 6.97626937,18.0113659 6.97626937,17.1045732 L6.97626937,7.04372085 C6.97626937,6.03310579 8.12740837,5.63666297 8.87912875,6.04517431 L18.1302326,11.0767578 C18.9581666,11.5241198 18.9555214,12.6266541 18.1302326,13.0740162 M23.3387109,0 L0.6612891,0 C0.297580095,0 0,0.297580095 0,0.6612891 L0,23.3387109 C0,23.7024199 0.297580095,24 0.6612891,24 L23.3387109,24 C23.7024199,24 24,23.7024199 24,23.3387109 L24,0.6612891 C24,0.297580095 23.7024199,0 23.3387109,0" fill="#555"></path>
+  </g>
+</svg></a>
+            <a href="https://www.linkedin.com/company/smud" target="_blank" class="footer-social-media-link gtm-footer-link-linkedin"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-linkedin-block" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <title id="LinkedinTitle">LinkedIn</title>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <rect fill="#FFFFFF" x="0" y="0" width="24" height="24"></rect>
+    <path d="M20.590274,20.6857973 L20.5864932,20.6857973 L17.0141096,20.6857973 L17.0141096,15.0750575 C17.0141096,13.7379616 16.9871507,12.0231123 15.1529589,12.0231123 C13.2916438,12.0231123 13.0064384,13.4759342 13.0064384,14.9787288 L13.0064384,20.6857973 L9.43043836,20.6857973 L9.43043836,9.15987945 L12.8639178,9.15987945 L12.8639178,10.7322082 L12.9140548,10.7322082 C13.3917534,9.82661918 14.5593699,8.87089315 16.3013425,8.87089315 C19.9235342,8.87089315 20.590274,11.256263 20.590274,14.3544 L20.590274,20.6857973 Z M5.39580822,7.58771507 C4.25120548,7.58771507 3.32260274,6.63971507 3.32260274,5.49527671 C3.32260274,4.35067397 4.25120548,3.42207123 5.39580822,3.42207123 C6.54024658,3.42207123 7.4690137,4.35067397 7.4690137,5.49527671 C7.4690137,6.63971507 6.54024658,7.58771507 5.39580822,7.58771507 L5.39580822,7.58771507 Z M3.60780822,20.6857973 L7.18775342,20.6857973 L7.18775342,9.15987945 L3.60780822,9.15987945 L3.60780822,20.6857973 Z M23.2307671,0.122071233 L0.682109589,0.122071233 C0.320465753,0.122071233 0.0245753425,0.417961644 0.0245753425,0.779605479 L0.0245753425,23.328263 C0.0245753425,23.6899068 0.320465753,23.9857973 0.682109589,23.9857973 L23.2307671,23.9857973 C23.592411,23.9857973 23.8883014,23.6899068 23.8883014,23.328263 L23.8883014,0.779605479 C23.8883014,0.417961644 23.592411,0.122071233 23.2307671,0.122071233 L23.2307671,0.122071233 Z" fill="#555"></path>
+  </g>
+</svg></a>
+            <a href="https://www.instagram.com/mysmud/" target="_blank" class="footer-social-media-link gtm-footer-link-instagram"><?xml version="1.0" encoding="UTF-8"?>
+<svg class="icon icon-instagram-block" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+  <title id="InstagramTitle">Instagram</title>
+  <defs>
+    <polygon points="6.57534247e-05 0 24 0 24 23.9999342 6.57534247e-05 23.9999342"></polygon>
+  </defs>
+  <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+    <rect fill="#FFFFFF" x="0" y="0" width="24" height="24"></rect>
+    <g>
+      <path d="M12.0019233,9.19609315 C10.4554027,9.19609315 9.19786849,10.453463 9.19786849,11.9999836 C9.19786849,13.5465041 10.4591836,14.803874 12.0019233,14.803874 C13.544663,14.803874 14.8058137,13.5465041 14.8058137,11.9999836 C14.8058137,10.453463 13.5482795,9.19609315 12.0019233,9.19609315" fill="#555"></path>
+      <path d="M15.4851452,7.51070137 C15.4851452,6.95524932 15.9355562,6.50483836 16.4911726,6.50483836 C17.0466247,6.50483836 17.4970356,6.95524932 17.4970356,7.51070137 C17.4970356,8.07009863 17.0466247,8.51672877 16.4911726,8.51672877 C15.9319397,8.51672877 15.4851452,8.06631781 15.4851452,7.51070137 Z M7.68909041,12.0000164 C7.68909041,9.61267397 9.61467945,7.68724932 12.0018575,7.68724932 C14.3892,7.68724932 16.314789,9.61267397 16.314789,12.0000164 C16.314789,14.3871945 14.3892,16.3127836 12.0018575,16.3127836 C9.61467945,16.3127836 7.68909041,14.3871945 7.68909041,12.0000164 Z M16.9603233,5.44259178 C15.8568164,5.00713973 13.2255288,5.10478356 12.0018575,5.10478356 C10.7781863,5.10478356 8.15084384,5.0033589 7.04339178,5.44259178 C6.30399452,5.73913973 5.7372,6.30593425 5.44443288,7.04155068 C5.00898082,8.14505753 5.10662466,10.7763452 5.10662466,12.0000164 C5.10662466,13.2236877 5.0052,15.8511945 5.44443288,16.9584822 C5.74098082,17.6978795 6.30777534,18.264674 7.04339178,18.5574411 C8.14689863,18.9928932 10.7781863,18.8952493 12.0018575,18.8952493 C13.2255288,18.8952493 15.8530356,18.9965096 16.9603233,18.5574411 C17.6998849,18.2608932 18.2665151,17.6940986 18.5592822,16.9584822 C18.9985151,15.8549753 18.8972548,13.2236877 18.8972548,12.0000164 C18.8972548,10.7763452 18.9985151,8.14883836 18.5592822,7.04155068 C18.2627342,6.30215342 17.6959397,5.7353589 16.9603233,5.44259178 Z" fill="#555"></path>
+      <path d="M20.3535452,15.468263 C20.2859836,16.8157151 19.9782575,18.0093041 18.9945863,18.992811 C18.0112438,19.9800986 16.8176548,20.2878247 15.4702027,20.3516055 C14.081326,20.4305096 9.9186411,20.4305096 8.52976438,20.3516055 C7.18231233,20.2840438 5.99250411,19.9763178 5.00521644,18.992811 C4.01809315,18.0093041 3.71036712,16.8157151 3.64642192,15.468263 C3.56768219,14.0793863 3.56768219,9.91686575 3.64642192,8.52798904 C3.71398356,7.18053699 4.01809315,5.98678356 5.00521644,5.0034411 C5.99250411,4.01993425 7.18592877,3.71220822 8.52976438,3.6484274 C9.9186411,3.56952329 14.081326,3.56952329 15.4702027,3.6484274 C16.8176548,3.71582466 18.0112438,4.02371507 18.9945863,5.00705753 C19.981874,5.99056438 20.2896,7.18415342 20.3535452,8.53176986 C20.4322849,9.92048219 20.4322849,14.0793863 20.3535452,15.468263 M23.3425315,-6.57534247e-05 L0.6576,-6.57534247e-05 C0.295791781,-6.57534247e-05 6.57534247e-05,0.295824658 6.57534247e-05,0.657468493 L6.57534247e-05,23.3424 C6.57534247e-05,23.7042082 0.295791781,23.9999342 0.6576,23.9999342 L23.3425315,23.9999342 C23.7041753,23.9999342 24.0000658,23.7042082 24.0000658,23.3424 L24.0000658,0.657468493 C24.0000658,0.295824658 23.7041753,-6.57534247e-05 23.3425315,-6.57534247e-05" fill="#555"></path>
+    </g>
+  </g>
+</svg></a>
+        </div>
+
+
+        <div class="col-12 d-block d-lg-none">
+            <span class="smaller-font">
+                &copy; 2024 Sacramento Municipal Utility District
+            </span>
+        </div>
+
+        <div class="col-12 d-block d-lg-none">
+            <span class="smaller-font">
+                <a href="https://www.smud.org/privacy" target="_blank" class="gtm-footer-link-privacy-policy" >Privacy Policy</a>
+                <svg class="footer-dot icon icon-dot" width="24px" height="24px" viewBox="0 0 24 24" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <path d="M12.0026513,16 C9.79623255,16 8,14.2041794 8,12.0004278 C8,9.7946228 9.79623255,8 12.0026513,8 C14.2073596,8 16,9.7946228 16,12.0004278 C16,14.2041794 14.2073596,16 12.0026513,16" id="dot-path-1"></path>
+    </defs>
+    <g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g>
+            <rect fill-rule="nonzero" x="0" y="0" width="24" height="24"></rect>
+            <mask id="mask-2" fill="white">
+                <use xlink:href="#path-1"></use>
+            </mask>
+            <use fill="currentColor" xlink:href="#dot-path-1"></use>
+        </g>
+    </g>
+</svg>
+            </span>
+            <span class="smaller-font">
+                <a href="https://www.smud.org/en/Corporate/Privacy-policy/Terms-and-conditions" target="_blank" class="gtm-footer-link-terms-and-conditions">Terms and conditions</a>
+            </span>
+        </div>
+
+            <div class="col-12 d-block d-lg-none">
+                <span class="smaller-font">
+                    <a href="/nav/togglelanguage">En espa&#241;ol</a>
+                </span>
+            </div>
+    </div>
+</footer>
+            </div>
+        </div>
+        <div id="other-elements-container" class="container-fluid p-0" role="contentinfo">
+            <div id="general-error-message" class="d-none" aria-labelledby="general-error-message">
+                <p class="text-center"><strong>Sorry, your request could not be processed. Please try again later.</strong></p>
+            </div>
+
+                <div id="feedback-survey" class="d-none d-lg-block">
+                    <div class="feedback-outter">
+                        <div class="feedback-inner">
+                            <button class="btn btn-primary survey-link">Give&nbsp;feedback</button>
+                        </div>
+                    </div>
+                </div>
+        </div>
+    </div>
+
+    <script src="/bundles/js-CMS?v=rGIbav8a4uyubXoRkdfkWg36v6MELsJ6MM5pEwkVFv81"></script>
+
+
+<script src="/bundles/js-signin-controller?v=xGfOteZOKOHr17MyT2oF2oFmRjClNqKJIBvpVijCDLg1"></script>
+
+
+
+
+<!-- Data layer used by Google Tag Manager -->
+<script>
+    var dataLayer = [];
+    dataLayer.push({
+
+                        'langCode': 'en'
+    });
+</script>
+
+
+<!-- Google Tag Manager -->
+<noscript aria-hidden="true">
+    <iframe src="//www.googletagmanager.com/ns.html?id=GTM-T23G5V" height="0" width="0" style="display:none;visibility:hidden"></iframe>
+</noscript>
+<script>
+    (function (w, d, s, l, i) {
+        w[l] = w[l] || []; w[l].push({
+            'gtm.start':
+                new Date().getTime(), event: 'gtm.js'
+        }); var f = d.getElementsByTagName(s)[0],
+            j = d.createElement(s), dl = l != 'dataLayer' ? '&l=' + l : ''; j.async = true; j.src =
+                '//www.googletagmanager.com/gtm.js?id=' + i + dl; f.parentNode.insertBefore(j, f);
+    })(window, document, 'script', 'dataLayer', 'GTM-T23G5V');
+</script>
+<!-- End Google Tag Manager -->
+
+
+
+<script src="https://www.smud.org/bundle/beacon"></script>
+
+
+
+        <script data-script-name="UsabillaScript">
+            /*{literal}<![CDATA[*/
+            window.lightningjs || function (c) {
+                function g(b, d) {
+                    d && (d += (/\?/.test(d) ? "&" : "?") + "lv=1"); c[b] || function () {
+                        var i = window, h = document, j = b, g = h.location.protocol, l = "load", k = 0; (function () {
+                            function b() { a.P(l); a.w = 1; c[j]("_load") } c[j] = function () { function m() { m.id = e; return c[j].apply(m, arguments) } var b, e = ++k; b = this && this != i ? this.id || 0 : 0; (a.s = a.s || []).push([e, b, arguments]); m.then = function (b, c, h) { var d = a.fh[e] = a.fh[e] || [], j = a.eh[e] = a.eh[e] || [], f = a.ph[e] = a.ph[e] || []; b && d.push(b); c && j.push(c); h && f.push(h); return m }; return m }; var a = c[j]._ = {}; a.fh = {}; a.eh = {}; a.ph = {}; a.l = d ? d.replace(/^\/\//, (g == "https:" ? g : "http:") + "//") : d; a.p = { 0: +new Date }; a.P = function (b) { a.p[b] = new Date - a.p[0] }; a.w && b(); i.addEventListener ? i.addEventListener(l, b, !1) : i.attachEvent("on" + l, b); var q = function () {
+                                function b() { return ["<head></head><", c, ' onload="var d=', n, ";d.getElementsByTagName('head')[0].", d, "(d.", g, "('script')).", i, "='", a.l, "'\"></", c, ">"].join("") } var c = "body", e = h[c]; if (!e) return setTimeout(q, 100); a.P(1); var d = "appendChild", g = "createElement", i = "src", k = h[g]("div"), l = k[d](h[g]("div")), f = h[g]("iframe"), n = "document", p; k.style.display = "none"; e.insertBefore(k, e.firstChild).id = o + "-" + j; f.frameBorder = "0"; f.id = o + "-frame-" + j; /MSIE[ ]+6/.test(navigator.userAgent) && (f[i] = "javascript:false"); f.allowTransparency = "true"; l[d](f); try { f.contentWindow[n].open() } catch (s) { a.domain = h.domain, p = "javascript:var d=" + n + ".open();d.domain='" + h.domain + "';", f[i] = p + "void(0);" } try { var r = f.contentWindow[n]; r.write(b()); r.close() } catch (t) {
+                                    f[i] = p + 'd.write("' + b().replace(/"/g, String.fromCharCode(92) + '"') + '");d.close();'
+                                } a.P(2)
+                            };
+                            a.l && setTimeout(q, 0)
+                        })()
+                    }(); c[b].lv = "1"; return c[b]
+                } var o = "lightningjs", k = window[o] = g(o); k.require = g; k.modules = c
+            }({}); if (!navigator.userAgent.match(/Android|BlackBerry|BB10|iPhone|iPad|iPod|Opera Mini|IEMobile/i)) { window.usabilla_live = lightningjs.require("usabilla_live", "//w.usabilla.com/77c45d49295c.js"); } else { window.usabilla_live = lightningjs.require("usabilla_live", "//w.usabilla.com/aa78769ac487.js"); }/*]]>{/literal}*/
+            window.usabilla_live("hide");
+            $(document).ready(function () {
+                $('.survey-link').on('click', function (e) {
+                    e.preventDefault();
+                    window.usabilla_live("click");
+                });
+            });
+        </script>
+    <script data-script-name="HotJarTrackingScript">
+        (function (h, o, t, j, a, r) {
+            h.hj = h.hj || function () { (h.hj.q = h.hj.q || []).push(arguments) };
+            h._hjSettings = { hjid: 436018, hjsv: 5 };
+            a = o.getElementsByTagName('head')[0];
+            r = o.createElement('script'); r.async = 1;
+            r.src = t + h._hjSettings.hjid + j + h._hjSettings.hjsv;
+            a.appendChild(r);
+        })(window, document, '//static.hotjar.com/c/hotjar-', '.js?sv=');
+    </script>
+
+</body>
+
+</html>
diff --git a/tests/opower/utilities/smud/smudOkta.html b/tests/opower/utilities/smud/smudOkta.html
new file mode 100644
index 0000000..3deb109
--- /dev/null
+++ b/tests/opower/utilities/smud/smudOkta.html
@@ -0,0 +1,68 @@
+<!DOCTYPE html><html lang="en"><head><meta http-equiv="Content-Type" content="text/html; charset=utf-8"/><meta name="robots" content="noarchive"/><meta name="googlebot" content="noarchive"/><meta name="robots" content="noindex,nofollow" />
+<meta http-equiv="X-UA-Compatible" content="IE=Edge"/>
+<meta name="apple-mobile-web-app-capable" content="yes">
+<meta name="googlebot" content="noindex"/>
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<meta name="robots" content="noindex,nofollow"/>
+
+<link href="https://ok2static.oktacdn.com/assets/img/icons/favicons/favicon-16x16.f0b05bcfd77a98675f317ee53d12da9b.png" rel="icon" type="image/png" sizes="16x16"/><link href="https://ok2static.oktacdn.com/assets/img/icons/favicons/favicon-32x32.62523a7499b3ab893e09a488b5a95751.png" rel="icon" type="image/png" sizes="32x32"/><link href="https://ok2static.oktacdn.com/assets/img/icons/favicons/favicon-96x96.c93fff4cee51afa555b05957847cbfa4.png" rel="icon" type="image/png" sizes="96x96"/><meta name="msapplication-TileColor" content="#ffffff">
+<meta name="msapplication-TileImage" content="/img/icons/favicons/ms-icon-144x144.png">
+<meta name="application-name" content="Okta"/>
+<meta name="theme-color" content="#ffffff">
+<meta name="msapplication-config" content="/img/icons/favicons/browserconfig.xml"/>
+
+<title>
+    Sacramento Municipal Utility District - Prod - Signing in...</title>
+<!--[if IE]><link href="https://ok2static.oktacdn.com/assets/css/ie/ie.8c35c7c3b048f3adcbc57c5313ac15f3.css" rel="stylesheet" type="text/css"/><![endif]-->
+<!--[if gte IE 9]><link href="https://ok2static.oktacdn.com/assets/css/ie/ie9.6ae4ed11f01b935887dcefa7b5357447.css" rel="stylesheet" type="text/css"/><![endif]-->
+
+<script type="text/javascript" nonce="B3FMqU-nnnTIEOyVdSxr0Q">window.cspNonce = 'B3FMqU-nnnTIEOyVdSxr0Q';</script><meta property="csp-nonce" content="B3FMqU-nnnTIEOyVdSxr0Q"/>
+
+<script type="text/javascript" nonce="B3FMqU-nnnTIEOyVdSxr0Q">if (typeof module === 'object') {window.module = module; module = undefined;}</script><script type="text/javascript" nonce="B3FMqU-nnnTIEOyVdSxr0Q">
+    var okta = {
+        migrateMute: true,
+        locale: 'en',
+        debug: false,
+        deployEnv: 'PROD',
+        userId: '00uxoh2vveLvVyYlU0x7',
+        settings: {
+            orgId: '00o14522edXGfVqUw0x7',
+            orgName: 'Sacramento\x20Municipal\x20Utility\x20District\x20\x2D\x20Prod',
+            serverStatus: 'ACTIVE',
+            persona: '',
+            isPreview: 'false' === 'true',
+            permissions: []
+        },
+        _links: {
+            oktaHelpLinkBase: '//help.okta.com/okta_help.htm?type=&locale=en&id=',
+            oktaPrivacyLink: 'https://okta.com/privacy-policy'
+        },
+        showPseudoLocModal: false,
+        enableIMESupport: false,
+        isEnableAdvancedSearchLuxon: true,
+        isEnableBcp47OktaUi: true,
+        theme: "",
+        dstheme_rebrand: true
+    };
+    </script><script nonce="B3FMqU-nnnTIEOyVdSxr0Q">var _features=["MONTHLY_FLAG_2018_10","MONTHLY_FLAG_2023_04","ENG_ENABLE_ACCESS_TOKEN_INTERNAL_APIS","MONTHLY_FLAG_2020_02","MONTHLY_FLAG_2022_10","ENDUSER_V2_OKTA_SOURCED","MONTHLY_FLAG_2019_12","MONTHLY_FLAG_2020_11","MONTHLY_FLAG_2023_05","MONTHLY_FLAG_2020_06","MONTHLY_FLAG_2020_07","MONTHLY_FLAG_2019_08","MONTHLY_FLAG_2019_05","MONTHLY_FLAG_2021_05","MONTHLY_FLAG_2022_08","MONTHLY_FLAG_2022_02","MONTHLY_FLAG_2020_09","MONTHLY_FLAG_2021_04","MONTHLY_FLAG_2021_12","MONTHLY_FLAG_2018_09","MONTHLY_FLAG_2022_03","MONTHLY_FLAG_2021_08","MONTHLY_FLAG_2020_12","MONTHLY_FLAG_2019_07","MONTHLY_FLAG_2019_06","MONTHLY_FLAG_2023_01","MONTHLY_FLAG_2022_11","ENG_ENABLE_ACCESS_TOKEN_SECTIONS_APIS","MONTHLY_FLAG_2018_11","MONTHLY_FLAG_2022_07","MONTHLY_FLAG_2023_06","ENG_ENABLE_AUTOMATED_SESSION_REVOCATION","MONTHLY_FLAG_2020_08","ENG_ENABLE_ACCESS_TOKEN_LOGS_APIS","MONTHLY_FLAG_2021_01","MONTHLY_FLAG_2020_05","MONTHLY_FLAG_2020_10","MONTHLY_FLAG_2021_02","MONTHLY_FLAG_2022_12","MONTHLY_FLAG_2019_04","MONTHLY_FLAG_2021_03","MONTHLY_FLAG_2019_10","MONTHLY_FLAG_2023_07","API_ACCESS_MANAGEMENT","MONTHLY_FLAG_2019_03","MONTHLY_FLAG_2020_03","MONTHLY_FLAG_2019_01","MONTHLY_FLAG_2021_06","MONTHLY_FLAG_2022_06","MONTHLY_FLAG_2022_04","MONTHLY_FLAG_2021_07","MONTHLY_FLAG_2019_02","MONTHLY_FLAG_2021_11","ENG_ENABLE_ACCESS_TOKEN_CATALOG_APIS","MONTHLY_FLAG_2019_11","MONTHLY_FLAG_2022_01","MONTHLY_FLAG_2023_03","MONTHLY_FLAG_2022_05","MONTHLY_FLAG_2019_09","PLUGIN_ONBOARDING","MONTHLY_FLAG_2018_12","MONTHLY_FLAG_2022_09","APP_NOTES","MONTHLY_FLAG_2020_01","MONTHLY_FLAG_2021_10","MONTHLY_FLAG_2021_09","MONTHLY_FLAG_2023_08","ENDUSER_V2_OKTA_MOBILE_ENHANCEMENTS","MONTHLY_FLAG_2023_02"];</script><script nonce="B3FMqU-nnnTIEOyVdSxr0Q">window.okta || (window.okta = {}); okta.cdnUrlHostname = "//ok2static.oktacdn.com"; okta.cdnPerformCheck = false;</script><script nonce="B3FMqU-nnnTIEOyVdSxr0Q">window.okta || (window.okta = {});window.okta.mixpanel = false;</script><script nonce="B3FMqU-nnnTIEOyVdSxr0Q" src="https://ok2static.oktacdn.com/assets/js/jquery-1.12.4.2ef93d9aedc4198ec425a799a371292d.js" crossorigin="anonymous" integrity="sha384-31M82v6kCQmIIoi5lnRQZCJcewLcyWMq5KJbo7NQ75AbNEtnT3hGMYEKYjax/UpQ" type="text/javascript"></script><link rel="chrome-webstore-item" href="https://chrome.google.com/webstore/detail/glnpjglilkicbckjpbgcfkogebgllemb">
+<!--[if lt IE 9]><script nonce="B3FMqU-nnnTIEOyVdSxr0Q" src="https://ok2static.oktacdn.com/assets/enduser/js/vendor/css3-mediaqueries.fa295f0132f5335f352071ca3613a94a.js" crossorigin="anonymous" integrity="sha384-7pU2GSgyec3nzQMUNSuzanfJelP9UCOyHil0bOv+WnPKSS9lNA/tcxPyr7NV2w6c" type="text/javascript"></script><![endif]-->
+
+<script type="text/javascript" nonce="B3FMqU-nnnTIEOyVdSxr0Q">if (window.module) module = window.module;</script></head>
+<body id="app" class="enduser-app  okta-legacy-theme dstheme_rebrand  is-enduser-v2 ">
+<noscript><div id="noscript-mask"></div><div id="noscript-msg" class="infobox infobox-warning infobox-compact"><span class="icon warning-16"></span><h3>Javascript is disabled on your browser.</h3><p>Please enable Javascript and refresh this page to use Okta.</p></div></noscript>
+
+<div id="container">
+<link href="https://ok2static.oktacdn.com/assets/css/sections/interstitial.c280c95e9e8c971dad6d6dd597ab23f8.css" rel="stylesheet" type="text/css"/><script type="text/javascript" nonce="B3FMqU-nnnTIEOyVdSxr0Q">
+    var interstitialMinWaitTime = 1200;
+    </script><div class="okta-iframe-overlay">
+                <iframe name="interstitial" src="https&#x3a;&#x2f;&#x2f;myaccount.smud.org&#x2f;signin&#x2f;ssotransition" frameborder="0" scrolling="auto"></iframe>
+            </div>
+        <form id="appForm" action="https&#x3a;&#x2f;&#x2f;sso.opower.com&#x2f;sp&#x2f;ACS.saml2" method="POST">
+    <input name="SAMLResponse" type="hidden" value="PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iVVRGLTgiPz48c2FtbDJwOlJlc3BvbnNlIERlc3RpbmF0aW9uPSJodHRwczovL3Nzby5vcG93ZXIuY29tL3NwL0FDUy5zYW1sMiIgSUQ9ImlkNDQ5MzczMDUwMzIxNDE0MjkyNTcxNzUxNDciIElzc3VlSW5zdGFudD0iMjAyNC0wMS0yMVQwMDoxODozMS4wMDdaIiBWZXJzaW9uPSIyLjAiIHhtbG5zOnNhbWwycD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnByb3RvY29sIiB4bWxuczp4cz0iaHR0cDovL3d3dy53My5vcmcvMjAwMS9YTUxTY2hlbWEiPjxzYW1sMjpJc3N1ZXIgRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6bmFtZWlkLWZvcm1hdDplbnRpdHkiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj5odHRwOi8vd3d3Lm9rdGEuY29tL2V4azJpaWUzdHRWbFc3aWNGMHg3PC9zYW1sMjpJc3N1ZXI&#x2b;PGRzOlNpZ25hdHVyZSB4bWxuczpkcz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnIyI&#x2b;PGRzOlNpZ25lZEluZm8&#x2b;PGRzOkNhbm9uaWNhbGl6YXRpb25NZXRob2QgQWxnb3JpdGhtPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48ZHM6U2lnbmF0dXJlTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxkc2lnLW1vcmUjcnNhLXNoYTI1NiIvPjxkczpSZWZlcmVuY2UgVVJJPSIjaWQ0NDkzNzMwNTAzMjE0MTQyOTI1NzE3NTE0NyI&#x2b;PGRzOlRyYW5zZm9ybXM&#x2b;PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyNlbnZlbG9wZWQtc2lnbmF0dXJlIi8&#x2b;PGRzOlRyYW5zZm9ybSBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyI&#x2b;PGVjOkluY2x1c2l2ZU5hbWVzcGFjZXMgUHJlZml4TGlzdD0ieHMiIHhtbG5zOmVjPSJodHRwOi8vd3d3LnczLm9yZy8yMDAxLzEwL3htbC1leGMtYzE0biMiLz48L2RzOlRyYW5zZm9ybT48L2RzOlRyYW5zZm9ybXM&#x2b;PGRzOkRpZ2VzdE1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZW5jI3NoYTI1NiIvPjxkczpEaWdlc3RWYWx1ZT5makY5ZzFJZS92VG9FbW9hVC9PTFl5ZnFiR1l6bjBXbjBucSt2OGxWU2Y0PTwvZHM6RGlnZXN0VmFsdWU&#x2b;PC9kczpSZWZlcmVuY2U&#x2b;PC9kczpTaWduZWRJbmZvPjxkczpTaWduYXR1cmVWYWx1ZT5GY0R2aDl1Wjg5Z3pmWE5ZNFBGNCtPMXR4UjVNbktwZVg1UEVWT2lXZG5PNFVIRncvS0xYZEpZUEhUeVdJTTdoUWduSFNXZ0tNNFFnSTl5NkVQUjRUVXh4cjVySnhuT2F4VTY2YkczT2pMWXRka0xMc2hJcUhqdmhTa2haM0FPRC9Rc3pwQ2hLSzFES3BBdHVhM3A2c01EWDF3dFVIWjhQVUVqQTFxeUs4YXYrNXp4N0hHMXBIeHdudU9DRm5FRE5lU29uRWo2TFNxVTNJV2pRSENFR0pVSGl0aWRVTWZObmZTb3Z4UVdqNVhoZStSNEpHWFBmWWhNcjVUQmo1T0ZmZUxUWXMwNjVaVkhjaWY1cjV5NFpvdEZ5V3dKakQyQkdOOHQwWFBnVDJwd0FVL0RhUWFzcWV6b2hhcTdOU0ZrTlJXTVloNFpXZjB0RSt1ZEUzbkZTSEE9PTwvZHM6U2lnbmF0dXJlVmFsdWU&#x2b;PGRzOktleUluZm8&#x2b;PGRzOlg1MDlEYXRhPjxkczpYNTA5Q2VydGlmaWNhdGU&#x2b;TUlJRG1EQ0NBb0NnQXdJQkFnSUdBVTZUWTBJaU1BMEdDU3FHU0liM0RRRUJCUVVBTUlHTU1Rc3dDUVlEVlFRR0V3SlZVekVUTUJFRwpBMVVFQ0F3S1EyRnNhV1p2Y201cFlURVdNQlFHQTFVRUJ3d05VMkZ1SUVaeVlXNWphWE5qYnpFTk1Bc0dBMVVFQ2d3RVQydDBZVEVVCk1CSUdBMVVFQ3d3TFUxTlBVSEp2ZG1sa1pYSXhEVEFMQmdOVkJBTU1CSE50ZFdReEhEQWFCZ2txaGtpRzl3MEJDUUVXRFdsdVptOUEKYjJ0MFlTNWpiMjB3SGhjTk1UVXdOekUxTWpBeU1qVXlXaGNOTkRVd056RTFNakF5TXpVeVdqQ0JqREVMTUFrR0ExVUVCaE1DVlZNeApFekFSQmdOVkJBZ01Da05oYkdsbWIzSnVhV0V4RmpBVUJnTlZCQWNNRFZOaGJpQkdjbUZ1WTJselkyOHhEVEFMQmdOVkJBb01CRTlyCmRHRXhGREFTQmdOVkJBc01DMU5UVDFCeWIzWnBaR1Z5TVEwd0N3WURWUVFEREFSemJYVmtNUnd3R2dZSktvWklodmNOQVFrQkZnMXAKYm1adlFHOXJkR0V1WTI5dE1JSUJJakFOQmdrcWhraUc5dzBCQVFFRkFBT0NBUThBTUlJQkNnS0NBUUVBbXYvOXRiWEpxeGRyZ0pTVQpiZVh0UVgwSENVNDU5Q2NCQ1R3eThXUklOM3d1VHVnMmxxSzViald2WmxldXhyQkxoZVAwYVp5TnNHREN6VXVDcUhUY0lmOXBwM2hBCjBHeE5BaFdDYStySWRSaStaSW55U1hFdVo2S2VIanJpQkpUeFZzMnFjUFpXKzBtR2s4TWI1TTUzMUdtbDJISnJ3YlcwYXNCeHNWTGEKNGxJb1dvWmM5K2xhdGtXamMvQ3YyZ2xaYUMvbVNiV3cyRElsOS9SYm1ienkxdUpHMy96OUVCM3F0dU5FT1NXL1BCYWRleitEM2dFbwpVTVNvMEc5Mi9QRHhUcGFXQ3RROEx2S0NpUXhDeFVGRm9FN2UzWDNXN3VVZ3JPV3M5bUVTSTJYVXZnWVB3Z2ZmOGh6NEpodjFWcnlzCmdHKzhBaTF5RW1PcDdJdHhOTFVXNndJREFRQUJNQTBHQ1NxR1NJYjNEUUVCQlFVQUE0SUJBUUFURHI2OUJlR2xlQmlFZlNWd25FRm8Kc3NJUU9ZaUFLYlBKbHFTMDg0VEczR3RoKzZIVWUrbFJjd3I4L1RLYVFveUJ4clB0V1cxa0swWGNBd3l1VjkxOXRXRDNxR0ZuL00xTApBa2VCOWpyRFFsVWFuUFA3aDF1SVpBRzVwYzB3bmlTajVvRVQxV2VpeWZnak8wTXNDSnl0Z3JDWkw3ZmR5OER5Rnl0c2M5UFpKTDd5ClozQmRWYkFuMWY1UzVtM216WHVEeGpiTmVESnR5c0dVUVQ5UXh0M3JiM1FYQjhSSkFOZElDdFFjT0pEclJVbmhzMzJYQW1QVW1RRXkKZHJGVGdNK0wrM2VuQkFOdUdUQ05LaGFMWEdsMXppYU15cWpaTzlDLzRVVGg2aHRScTROVW9xQVNFa05mU2FZdkhFWXVsRTNRMDRYLwphOWduVkkwNkF5cVU3ZUFyPC9kczpYNTA5Q2VydGlmaWNhdGU&#x2b;PC9kczpYNTA5RGF0YT48L2RzOktleUluZm8&#x2b;PC9kczpTaWduYXR1cmU&#x2b;PHNhbWwycDpTdGF0dXMgeG1sbnM6c2FtbDJwPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6cHJvdG9jb2wiPjxzYW1sMnA6U3RhdHVzQ29kZSBWYWx1ZT0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOnN0YXR1czpTdWNjZXNzIi8&#x2b;PC9zYW1sMnA6U3RhdHVzPjxzYW1sMjpBc3NlcnRpb24gSUQ9ImlkNDQ5MzczMDUwMzIyODE4NjMxOTk1NTExNDYxIiBJc3N1ZUluc3RhbnQ9IjIwMjQtMDEtMjFUMDA6MTg6MzEuMDA3WiIgVmVyc2lvbj0iMi4wIiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiIgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIj48c2FtbDI6SXNzdWVyIEZvcm1hdD0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOm5hbWVpZC1mb3JtYXQ6ZW50aXR5IiB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI&#x2b;aHR0cDovL3d3dy5va3RhLmNvbS9leGsyaWllM3R0VmxXN2ljRjB4Nzwvc2FtbDI6SXNzdWVyPjxkczpTaWduYXR1cmUgeG1sbnM6ZHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvMDkveG1sZHNpZyMiPjxkczpTaWduZWRJbmZvPjxkczpDYW5vbmljYWxpemF0aW9uTWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIi8&#x2b;PGRzOlNpZ25hdHVyZU1ldGhvZCBBbGdvcml0aG09Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMDQveG1sZHNpZy1tb3JlI3JzYS1zaGEyNTYiLz48ZHM6UmVmZXJlbmNlIFVSST0iI2lkNDQ5MzczMDUwMzIyODE4NjMxOTk1NTExNDYxIj48ZHM6VHJhbnNmb3Jtcz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMC8wOS94bWxkc2lnI2VudmVsb3BlZC1zaWduYXR1cmUiLz48ZHM6VHJhbnNmb3JtIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8xMC94bWwtZXhjLWMxNG4jIj48ZWM6SW5jbHVzaXZlTmFtZXNwYWNlcyBQcmVmaXhMaXN0PSJ4cyIgeG1sbnM6ZWM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvMTAveG1sLWV4Yy1jMTRuIyIvPjwvZHM6VHJhbnNmb3JtPjwvZHM6VHJhbnNmb3Jtcz48ZHM6RGlnZXN0TWV0aG9kIEFsZ29yaXRobT0iaHR0cDovL3d3dy53My5vcmcvMjAwMS8wNC94bWxlbmMjc2hhMjU2Ii8&#x2b;PGRzOkRpZ2VzdFZhbHVlPjc4ckY0QWVTNEJOUkxJK2JQNVUyTmFuTmxtR1pVa0RWQk1LUkVhTXNxYlE9PC9kczpEaWdlc3RWYWx1ZT48L2RzOlJlZmVyZW5jZT48L2RzOlNpZ25lZEluZm8&#x2b;PGRzOlNpZ25hdHVyZVZhbHVlPlI3NlJHVGdRZzJLWTFTdEFqQ21XblNPYjgyVWxhVmhYVkpBKy9jYzA4ZUtiaS8xenVqMGNnK2s1T2FPdWorL2cwQU5aeUVjNFVESnNHbTNSWDliZVNyZ21pU0xZTUY0aTZnd01HZ1VYM0VzQVdCYU1ta0llWjVHdFJ6clVKZUR3ZFFEeUJWTVBhdmxNRGVQZ2RhYlJwbytQdDJRNUJ4bHRWVVkxdWtoS0NYaVQ1TloxZmROU1NJMHRPNVAwMEtxNVluZk5Gb3gxQVhqaENPcjdLQXBFeFpWdUR6VmNURStrZHl4R0h2WVpuQmg1QS9reEpMd05JV1MxRjMybUZpblA1RWY1SG9wS2tmYjZGeXlxVXB0TUFnamRJTlFjMjd6QzR3TktjTXoreUdJTVluSUtZdUp3QjNsYWVNYnpEdlRadlFtMXdMUFNITDJvajl3YjhLTFlUZz09PC9kczpTaWduYXR1cmVWYWx1ZT48ZHM6S2V5SW5mbz48ZHM6WDUwOURhdGE&#x2b;PGRzOlg1MDlDZXJ0aWZpY2F0ZT5NSUlEbURDQ0FvQ2dBd0lCQWdJR0FVNlRZMElpTUEwR0NTcUdTSWIzRFFFQkJRVUFNSUdNTVFzd0NRWURWUVFHRXdKVlV6RVRNQkVHCkExVUVDQXdLUTJGc2FXWnZjbTVwWVRFV01CUUdBMVVFQnd3TlUyRnVJRVp5WVc1amFYTmpiekVOTUFzR0ExVUVDZ3dFVDJ0MFlURVUKTUJJR0ExVUVDd3dMVTFOUFVISnZkbWxrWlhJeERUQUxCZ05WQkFNTUJITnRkV1F4SERBYUJna3Foa2lHOXcwQkNRRVdEV2x1Wm05QQpiMnQwWVM1amIyMHdIaGNOTVRVd056RTFNakF5TWpVeVdoY05ORFV3TnpFMU1qQXlNelV5V2pDQmpERUxNQWtHQTFVRUJoTUNWVk14CkV6QVJCZ05WQkFnTUNrTmhiR2xtYjNKdWFXRXhGakFVQmdOVkJBY01EVk5oYmlCR2NtRnVZMmx6WTI4eERUQUxCZ05WQkFvTUJFOXIKZEdFeEZEQVNCZ05WQkFzTUMxTlRUMUJ5YjNacFpHVnlNUTB3Q3dZRFZRUUREQVJ6YlhWa01Sd3dHZ1lKS29aSWh2Y05BUWtCRmcxcApibVp2UUc5cmRHRXVZMjl0TUlJQklqQU5CZ2txaGtpRzl3MEJBUUVGQUFPQ0FROEFNSUlCQ2dLQ0FRRUFtdi85dGJYSnF4ZHJnSlNVCmJlWHRRWDBIQ1U0NTlDY0JDVHd5OFdSSU4zd3VUdWcybHFLNWJqV3ZabGV1eHJCTGhlUDBhWnlOc0dEQ3pVdUNxSFRjSWY5cHAzaEEKMEd4TkFoV0NhK3JJZFJpK1pJbnlTWEV1WjZLZUhqcmlCSlR4VnMycWNQWlcrMG1HazhNYjVNNTMxR21sMkhKcndiVzBhc0J4c1ZMYQo0bElvV29aYzkrbGF0a1dqYy9DdjJnbFphQy9tU2JXdzJESWw5L1JibWJ6eTF1SkczL3o5RUIzcXR1TkVPU1cvUEJhZGV6K0QzZ0VvClVNU28wRzkyL1BEeFRwYVdDdFE4THZLQ2lReEN4VUZGb0U3ZTNYM1c3dVVnck9XczltRVNJMlhVdmdZUHdnZmY4aHo0Smh2MVZyeXMKZ0crOEFpMXlFbU9wN0l0eE5MVVc2d0lEQVFBQk1BMEdDU3FHU0liM0RRRUJCUVVBQTRJQkFRQVREcjY5QmVHbGVCaUVmU1Z3bkVGbwpzc0lRT1lpQUtiUEpscVMwODRURzNHdGgrNkhVZStsUmN3cjgvVEthUW95QnhyUHRXVzFrSzBYY0F3eXVWOTE5dFdEM3FHRm4vTTFMCkFrZUI5anJEUWxVYW5QUDdoMXVJWkFHNXBjMHduaVNqNW9FVDFXZWl5ZmdqTzBNc0NKeXRnckNaTDdmZHk4RHlGeXRzYzlQWkpMN3kKWjNCZFZiQW4xZjVTNW0zbXpYdUR4amJOZURKdHlzR1VRVDlReHQzcmIzUVhCOFJKQU5kSUN0UWNPSkRyUlVuaHMzMlhBbVBVbVFFeQpkckZUZ00rTCszZW5CQU51R1RDTktoYUxYR2wxemlhTXlxalpPOUMvNFVUaDZodFJxNE5Vb3FBU0VrTmZTYVl2SEVZdWxFM1EwNFgvCmE5Z25WSTA2QXlxVTdlQXI8L2RzOlg1MDlDZXJ0aWZpY2F0ZT48L2RzOlg1MDlEYXRhPjwvZHM6S2V5SW5mbz48L2RzOlNpZ25hdHVyZT48c2FtbDI6U3ViamVjdCB4bWxuczpzYW1sMj0idXJuOm9hc2lzOm5hbWVzOnRjOlNBTUw6Mi4wOmFzc2VydGlvbiI&#x2b;PHNhbWwyOk5hbWVJRCBGb3JtYXQ9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjEuMTpuYW1laWQtZm9ybWF0OnVuc3BlY2lmaWVkIj4wMDEwNTY4MjU5PC9zYW1sMjpOYW1lSUQ&#x2b;PHNhbWwyOlN1YmplY3RDb25maXJtYXRpb24gTWV0aG9kPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6Y206YmVhcmVyIj48c2FtbDI6U3ViamVjdENvbmZpcm1hdGlvbkRhdGEgTm90T25PckFmdGVyPSIyMDI0LTAxLTIxVDAwOjIzOjMxLjAwOFoiIFJlY2lwaWVudD0iaHR0cHM6Ly9zc28ub3Bvd2VyLmNvbS9zcC9BQ1Muc2FtbDIiLz48L3NhbWwyOlN1YmplY3RDb25maXJtYXRpb24&#x2b;PC9zYW1sMjpTdWJqZWN0PjxzYW1sMjpDb25kaXRpb25zIE5vdEJlZm9yZT0iMjAyNC0wMS0yMVQwMDoxMzozMS4wMDhaIiBOb3RPbk9yQWZ0ZXI9IjIwMjQtMDEtMjFUMDA6MjM6MzEuMDA4WiIgeG1sbnM6c2FtbDI9InVybjpvYXNpczpuYW1lczp0YzpTQU1MOjIuMDphc3NlcnRpb24iPjxzYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjxzYW1sMjpBdWRpZW5jZT5zc28ub3Bvd2VyLmNvbTwvc2FtbDI6QXVkaWVuY2U&#x2b;PC9zYW1sMjpBdWRpZW5jZVJlc3RyaWN0aW9uPjwvc2FtbDI6Q29uZGl0aW9ucz48c2FtbDI6QXV0aG5TdGF0ZW1lbnQgQXV0aG5JbnN0YW50PSIyMDI0LTAxLTIxVDAwOjE4OjMxLjAwN1oiIFNlc3Npb25JbmRleD0iaWQxNzA1Nzk2MzExMDA2LjEwMjU4ODQzNDAiIHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXV0aG5Db250ZXh0PjxzYW1sMjpBdXRobkNvbnRleHRDbGFzc1JlZj51cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YWM6Y2xhc3NlczpQYXNzd29yZFByb3RlY3RlZFRyYW5zcG9ydDwvc2FtbDI6QXV0aG5Db250ZXh0Q2xhc3NSZWY&#x2b;PC9zYW1sMjpBdXRobkNvbnRleHQ&#x2b;PC9zYW1sMjpBdXRoblN0YXRlbWVudD48c2FtbDI6QXR0cmlidXRlU3RhdGVtZW50IHhtbG5zOnNhbWwyPSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXNzZXJ0aW9uIj48c2FtbDI6QXR0cmlidXRlIE5hbWU9InNlbGVjdGVkQWNjb3VudElkIiBOYW1lRm9ybWF0PSJ1cm46b2FzaXM6bmFtZXM6dGM6U0FNTDoyLjA6YXR0cm5hbWUtZm9ybWF0OmJhc2ljIj48c2FtbDI6QXR0cmlidXRlVmFsdWUgeG1sbnM6eHM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hIiB4bWxuczp4c2k9Imh0dHA6Ly93d3cudzMub3JnLzIwMDEvWE1MU2NoZW1hLWluc3RhbmNlIiB4c2k6dHlwZT0ieHM6c3RyaW5nIj4wMDAwMDY5NDE1MjMtMDAwMDIwMjAxNjwvc2FtbDI6QXR0cmlidXRlVmFsdWU&#x2b;PC9zYW1sMjpBdHRyaWJ1dGU&#x2b;PC9zYW1sMjpBdHRyaWJ1dGVTdGF0ZW1lbnQ&#x2b;PC9zYW1sMjpBc3NlcnRpb24&#x2b;PC9zYW1sMnA6UmVzcG9uc2U&#x2b;"/>
+    <input name="RelayState" type="hidden" value="https&#x3a;&#x2f;&#x2f;smud.opower.com&#x2f;ei&#x2f;app&#x2f;myEnergyUse"/>
+</form>
+
+<script nonce="B3FMqU-nnnTIEOyVdSxr0Q" src="https://ok2static.oktacdn.com/assets/js/app/sso/interstitial.3bb1b1dab07d5557b70fe16c1da0276e.js" crossorigin="anonymous" integrity="sha384-2WcW5YstloCpzr/FoR1lBH4xhOsdIomkmfTMnjTlJJ2KnvbTG/l/HUvuqgCpGSFB" type="text/javascript"></script></div>
+<!-- close #container -->
+</body>
+</html>
diff --git a/tests/opower/utilities/test_smud.py b/tests/opower/utilities/test_smud.py
new file mode 100644
index 0000000..f8d4446
--- /dev/null
+++ b/tests/opower/utilities/test_smud.py
@@ -0,0 +1,95 @@
+"""Tests for SMUD."""
+import os
+import unittest
+
+import aiohttp
+from dotenv import load_dotenv
+from yarl import URL
+
+from opower.utilities.smud import (
+    SMUD,
+    SMUDLoginParser,
+    SMUDOktaResponseSamlResponseValueParser,
+)
+
+REQUEST_VERIFICATION_TOKEN_HTML_FILENAME = os.path.join(
+    os.path.dirname(__file__), "smud/myAccountResponse.html"
+)
+OKTA_SAML_RESPONSE_HTML_FILENAME = os.path.join(
+    os.path.dirname(__file__), "smud/smudOkta.html"
+)
+ENV_SECRET_PATH = os.path.join(os.path.dirname(__file__), "../../../.env.secret")
+
+
+class TestSMUD(unittest.IsolatedAsyncioTestCase):
+    """Test public methods inherited from UtilityBase."""
+
+    def test_name(self) -> None:
+        """Test name."""
+        smud = SMUD()
+
+        self.assertEqual("Sacramento Municipal Utility District (SMUD)", smud.name())
+
+    def test_subdomain(self) -> None:
+        """Test subdomain."""
+        smud = SMUD()
+
+        self.assertEqual("smud", smud.subdomain())
+
+    def test_timezone(self) -> None:
+        """Test timezone."""
+        smud = SMUD()
+
+        self.assertEqual("America/Los_Angeles", smud.timezone())
+
+    async def test_real_login(self) -> None:
+        """Perform a live test against the SMUD, OKTA and Opower websites."""
+        load_dotenv(dotenv_path=ENV_SECRET_PATH)
+
+        username = os.getenv("SMUD_USERNAME")
+        password = os.getenv("SMUD_PASSWORD")
+
+        if username is None or password is None:
+            self.skipTest(
+                "Add `SMUD_USERNAME=` and `SMUD_PASSWORD=` to `.env.secret` to run live SMUD test."
+            )
+
+        smud = SMUD()
+        session = aiohttp.ClientSession()
+        self.addCleanup(session.close)
+
+        await smud.async_login(session, username, password, None)
+
+        # Confirm opower cookies have been set.
+        self.assertTrue(
+            len(session.cookie_jar.filter_cookies(URL("https://smud.opower.com/ei")))
+            > 0
+        )
+
+
+class TestSMUDLoginParser(unittest.TestCase):
+    """Test parsing the Request Verification Token from the SMUD login page."""
+
+    def test_parse(self) -> None:
+        """Test finding the input __RequestVerificationToken value."""
+        loginParser = SMUDLoginParser()
+        html = open(REQUEST_VERIFICATION_TOKEN_HTML_FILENAME).read()
+        loginParser.feed(html)
+        result = loginParser.verification_token
+        assert result is not None
+        self.assertEqual("O04jy", result[0:5])
+        self.assertEqual("5fWQ1", result[-5:])
+
+
+class TestSMUDOktaResponseSamlResponseValueParser(unittest.TestCase):
+    """Test parsing the SamlResponse input value from the OKTA HTML."""
+
+    def test_parse(self) -> None:
+        """Test parsing the input SamlResponse value."""
+        samlResponseParser = SMUDOktaResponseSamlResponseValueParser()
+        html = open(OKTA_SAML_RESPONSE_HTML_FILENAME).read()
+        samlResponseParser.feed(html)
+        result = samlResponseParser.saml_response
+        assert result is not None
+        self.assertEqual("PD94b", result[0:5])
+        self.assertEqual("uc2U+", result[-5:])