packages feed

home-assistant-client (empty) → 0.1.0.0

raw patch · 12 files changed

+762/−0 lines, 12 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, containers, deriving-aeson, exceptions, home-assistant-client, http-client, http-client-tls, http-types, optparse-applicative, servant, servant-client, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog for `home-assistant-client`++All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),+and this project adheres to the+[Haskell Package Versioning Policy](https://pvp.haskell.org/).++## 0.1.0.0 - 2025-08-10++- Initial version with partial support for the Home Assistant REST API
+ LICENSE view
@@ -0,0 +1,26 @@+Copyright 2025 Michael B. Gale++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1.  Redistributions of source code must retain the above copyright notice, this+    list of conditions and the following disclaimer.++2.  Redistributions in binary form must reproduce the above copyright notice,+    this list of conditions and the following disclaimer in the documentation+    and/or other materials provided with the distribution.++3.  Neither the name of the copyright holder nor the names of its contributors+    may be used to endorse or promote products derived from this software+    without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,36 @@+# home-assistant-client: Haskell client for the Home Assistant API++![GitHub](https://img.shields.io/github/license/mbg/home-assistant-client)+[![Haskell](https://github.com/mbg/home-assistant-client/actions/workflows/haskell.yml/badge.svg)](https://github.com/mbg/home-assistant-client/actions/workflows/haskell.yml)+[![Stackage Nightly](https://github.com/mbg/home-assistant-client/actions/workflows/nightly.yml/badge.svg)](https://github.com/mbg/home-assistant-client/actions/workflows/nightly.yml)++A Haskell library for the [Home Assistant API](https://developers.home-assistant.io/docs/api/rest/), allowing you to write Haskell applications which interact with [Home Assistant](https://www.home-assistant.io).++A `ha-client` program that exposes the library functions as CLI commands is also provided.++## Library usage++The Home Assistant API offers fairly general endpoints, for which the `HomeAssistant.Client` module provides client computations. All API endpoints require authorisation with a [long-lived token](https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token). For example, to retrieve a list available services from Home Assistant:++```haskell+import Network.HTTP.Client++defaultAddress :: String+defaultAddress = "http://homeassistant.local:8123/"++main :: IO ()+main = do+    -- `token` should be the long-lived token+    httpManager <- defaultManagerSettings+    address <- parseBaseUrl defaultAddress++    let env = mkHomeAssistantEnv token httpManager address+    result <- runClientM services env+    print result+```++## CLI usage++You must set an environment variable named `HA_TOKEN` to your [long-lived token](https://developers.home-assistant.io/docs/auth_api/#long-lived-access-token). The CLI also inspects the `HA_ADDRESS` environment variable to determine the address of the Home Assistant server. This is optional and will default to `http://homeassistant.local:8123/` if no value is set.++The `ha-client` CLI can be invoked with `--help` to retrieve usage information. All sub-commands also support the `--help` flag to retrieve command-specific help.
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ app/Main.hs view
@@ -0,0 +1,88 @@+module Main (main) where++--------------------------------------------------------------------------------++import Control.Monad.IO.Class++import qualified Data.Aeson as JSON+import qualified Data.ByteString.Char8 as C8+import qualified Data.ByteString.Lazy as LBS+import Data.Maybe++import System.Environment+import System.Exit++import Network.HTTP.Client+import Network.HTTP.Client.TLS++import HomeAssistant.Client+import HomeAssistant.Common.HomeAssistant+import HomeAssistant.Common.Notify++import Options++--------------------------------------------------------------------------------++-- | This is the default address that Home Assistant installations can be found at.+-- Used only if no address is explicitly specified.+defaultAddress :: String+defaultAddress = "http://homeassistant.local:8123/"++--------------------------------------------------------------------------------++managerForBaseUrl :: BaseUrl -> IO Manager+managerForBaseUrl address = newManager $ case baseUrlScheme address of+    Http -> defaultManagerSettings+    Https -> tlsManagerSettings++--------------------------------------------------------------------------------++runAndFormat :: JSON.ToJSON v => ClientEnv -> HA v -> IO ()+runAndFormat env m = runClientM m env >>= \case+    Left err -> print err+    Right v -> LBS.putStr (JSON.encode v)++runConfigCmd :: ClientEnv -> ConfigCommand -> IO ()+runConfigCmd env ShowConfig = runAndFormat env config+runConfigCmd env CheckConfig = runAndFormat env $ do+    result <- check+    case result of+        JSON.Error err -> liftIO $ do+            print err+            exitFailure+        JSON.Success v -> pure v+runConfigCmd env ReloadAllConfig = runAndFormat env reloadAll++runServiceCmd :: ClientEnv -> ServiceCommand -> IO ()+runServiceCmd env ListServices = runAndFormat env services++runNotificationCmd :: ClientEnv -> NotificationCommand -> IO ()+runNotificationCmd env SendNotification{..} = runAndFormat env $ do+    let notificationData = OtherNotificationData <$> sendNotificationData+        payload = setNotificationData notificationData $ notification sendNotificationMessage+    notify sendNotificationDevice payload++-- | 'main' is the main entry point for this application.+main :: IO ()+main = do+    -- Parse command-line arguments+    opts <- parseCmdLineArgs++    mToken <- fmap C8.pack <$> lookupEnv "HA_TOKEN"+    address <- fromMaybe defaultAddress <$> lookupEnv "HA_ADDRESS" >>= parseBaseUrl++    case mToken of+        Nothing -> do+            putStrLn "Error: HA_TOKEN must be set"+            exitFailure+        Just token -> do+            httpManager <- managerForBaseUrl address++            let env = mkHomeAssistantEnv token httpManager address++            case clientOptionsCommand opts of+                ConfigCommand cmd -> runConfigCmd env cmd+                ServiceCommand cmd -> runServiceCmd env cmd+                NotificationCommand cmd -> runNotificationCmd env cmd++--------------------------------------------------------------------------------
+ app/Options.hs view
@@ -0,0 +1,110 @@+-- | Contains types representing the command-line configuration options of the CLI+-- along with parsers to parse command-line arguments into configuration values.+module Options (+    ConfigCommand(..),+    ServiceCommand(..),+    NotificationCommand(..),+    ClientCommand(..),+    ClientOptions(..),+    parseCmdLineArgs+) where++--------------------------------------------------------------------------------++import Data.Aeson+import Data.Text+import Data.ByteString.Lazy.Char8 qualified as C8++import Options.Applicative++--------------------------------------------------------------------------------++-- | Represents commands related to the Home Assistant configuration.+data ConfigCommand+    = ShowConfig+    | CheckConfig+    | ReloadAllConfig++showConfigP :: Parser ConfigCommand+showConfigP = pure ShowConfig++checkConfigP :: Parser ConfigCommand+checkConfigP = pure ShowConfig++reloadAllConfigP :: Parser ConfigCommand+reloadAllConfigP = pure ReloadAllConfig++--------------------------------------------------------------------------------++-- | Represents commands related to Home Assistant services.+data ServiceCommand+    = ListServices++listServicesP :: Parser ServiceCommand+listServicesP = pure ListServices++--------------------------------------------------------------------------------++-- | Represents commands related to the notification service.+data NotificationCommand+    = SendNotification {+        -- | The name of the service to send the notification to.+        sendNotificationDevice :: Text,+        -- | The message to send.+        sendNotificationMessage :: Text,+        -- | Arbitrary data to send.+        sendNotificationData :: Maybe Value+    }++sendNotificationP :: Parser NotificationCommand+sendNotificationP = SendNotification <$>+    argument str (metavar "DEVICE") <*>+    argument str (metavar "MESSAGE") <*>+    optional (option (eitherReader (eitherDecode . C8.pack)) (long "data"))++--------------------------------------------------------------------------------++data ClientCommand+    = ConfigCommand ConfigCommand+    | ServiceCommand ServiceCommand+    | NotificationCommand NotificationCommand++configP :: Parser ClientCommand+configP = fmap ConfigCommand $ subparser $+    command "show" (info (showConfigP <**> helper) (progDesc "Show the Home Assistant configuration")) <>+    command "check" (info (checkConfigP <**> helper) (progDesc "Check the Home Assistant configuration")) <>+    command "reload-all" (info (reloadAllConfigP <**> helper) (progDesc "Reloads all Home Assistant configurations"))++servicesP :: Parser ClientCommand+servicesP = fmap ServiceCommand $ subparser $+    command "list" (info (listServicesP <**> helper) (progDesc "Lists all Home Assistant services"))++notificationP :: Parser ClientCommand+notificationP = fmap NotificationCommand $ subparser $+    command "send" (info (sendNotificationP <**> helper) (progDesc "Send a notification"))++clientCommandP :: Parser ClientCommand+clientCommandP = subparser $+    command "config" (info (configP <**> helper) (progDesc "Configuration commands")) <>+    command "services" (info (servicesP <**> helper) (progDesc "Services commands")) <>+    command "notification" (info (notificationP <**> helper) (progDesc "Notification service commands"))++--------------------------------------------------------------------------------++data ClientOptions = MkClientOptions {+    clientAddress :: Maybe Text,+    clientOptionsCommand :: ClientCommand+}++optionsP :: Parser ClientOptions+optionsP = MkClientOptions+ <$> optional (strOption (long "address" <> metavar "URL" <> help "The address of the Home Assistant server"))+ <*> clientCommandP++opts :: ParserInfo ClientOptions+opts = info (optionsP <**> helper) idm++parseCmdLineArgs :: IO ClientOptions+parseCmdLineArgs = execParser opts++--------------------------------------------------------------------------------
+ home-assistant-client.cabal view
@@ -0,0 +1,144 @@+cabal-version: 2.2++-- This file has been generated from package.yaml by hpack version 0.38.1.+--+-- see: https://github.com/sol/hpack++name:           home-assistant-client+version:        0.1.0.0+synopsis:       Client library for the Home Assistant API.+description:    Please see the README on GitHub at <https://github.com/mbg/home-assistant-client#readme>+category:       Web+homepage:       https://github.com/mbg/home-assistant-client#readme+bug-reports:    https://github.com/mbg/home-assistant-client/issues+author:         Michael B. Gale+maintainer:     github@michael-gale.co.uk+copyright:      2025 Michael B. Gale+license:        BSD-3-Clause+license-file:   LICENSE+build-type:     Simple+extra-source-files:+    README.md+extra-doc-files:+    CHANGELOG.md++source-repository head+  type: git+  location: https://github.com/mbg/home-assistant-client++library+  exposed-modules:+      HomeAssistant.Client+      HomeAssistant.Common.HomeAssistant+      HomeAssistant.Common.Notify+      HomeAssistant.Types+  other-modules:+      Paths_home_assistant_client+  autogen-modules:+      Paths_home_assistant_client+  hs-source-dirs:+      src+  default-extensions:+      DataKinds+      DeriveGeneric+      DerivingVia+      FlexibleInstances+      GADTs+      ImportQualifiedPost+      KindSignatures+      LambdaCase+      OverloadedStrings+      RecordWildCards+      TypeOperators+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints+  build-depends:+      aeson >=2.2.3 && <2.3+    , base >=4.7 && <5+    , bytestring >=0.12.1 && <0.13+    , containers >=0.6.8 && <0.8+    , deriving-aeson >=0.2.10 && <0.3+    , exceptions >=0.10.9 && <0.11+    , http-client >=0.7.19 && <0.8+    , http-client-tls >=0.3.6 && <0.4+    , http-types >=0.12.4 && <0.13+    , servant >=0.20.3 && <0.21+    , servant-client >=0.20.3 && <0.21+    , text >=2.1.1 && <2.2+  default-language: Haskell2010++executable ha-client+  main-is: Main.hs+  other-modules:+      Options+      Paths_home_assistant_client+  autogen-modules:+      Paths_home_assistant_client+  hs-source-dirs:+      app+  default-extensions:+      DataKinds+      DeriveGeneric+      DerivingVia+      FlexibleInstances+      GADTs+      ImportQualifiedPost+      KindSignatures+      LambdaCase+      OverloadedStrings+      RecordWildCards+      TypeOperators+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson >=2.2.3 && <2.3+    , base >=4.7 && <5+    , bytestring >=0.12.1 && <0.13+    , containers >=0.6.8 && <0.8+    , deriving-aeson >=0.2.10 && <0.3+    , exceptions >=0.10.9 && <0.11+    , home-assistant-client+    , http-client >=0.7.19 && <0.8+    , http-client-tls >=0.3.6 && <0.4+    , http-types >=0.12.4 && <0.13+    , optparse-applicative >=0.18.0 && <0.20+    , servant >=0.20.3 && <0.21+    , servant-client >=0.20.3 && <0.21+    , text >=2.1.1 && <2.2+  default-language: Haskell2010++test-suite home-assistant-client-test+  type: exitcode-stdio-1.0+  main-is: Spec.hs+  other-modules:+      Paths_home_assistant_client+  autogen-modules:+      Paths_home_assistant_client+  hs-source-dirs:+      test+  default-extensions:+      DataKinds+      DeriveGeneric+      DerivingVia+      FlexibleInstances+      GADTs+      ImportQualifiedPost+      KindSignatures+      LambdaCase+      OverloadedStrings+      RecordWildCards+      TypeOperators+  ghc-options: -Wall -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N+  build-depends:+      aeson >=2.2.3 && <2.3+    , base >=4.7 && <5+    , bytestring >=0.12.1 && <0.13+    , containers >=0.6.8 && <0.8+    , deriving-aeson >=0.2.10 && <0.3+    , exceptions >=0.10.9 && <0.11+    , home-assistant-client+    , http-client >=0.7.19 && <0.8+    , http-client-tls >=0.3.6 && <0.4+    , http-types >=0.12.4 && <0.13+    , servant >=0.20.3 && <0.21+    , servant-client >=0.20.3 && <0.21+    , text >=2.1.1 && <2.2+  default-language: Haskell2010
+ src/HomeAssistant/Client.hs view
@@ -0,0 +1,98 @@+-- | Client library for the Home Assistant API.+module HomeAssistant.Client (+    API,+    HA,+    JSONOptions,+    StateChange,+    status,+    config,+    services,+    callService,+    mkHomeAssistantEnv,+    module Servant.Client+) where++--------------------------------------------------------------------------------++import Data.Aeson+import Data.Data+import Data.Text+import Data.ByteString.Char8 qualified as C8++import Deriving.Aeson++import Network.HTTP.Client (Manager, requestHeaders)+import Network.HTTP.Types++import Servant.API+import Servant.Client++import HomeAssistant.Types++--------------------------------------------------------------------------------++-- | The Home Assistant API as a type.+type API = "api" :> Endpoints+type Endpoints+    = Get '[JSON] Value+ :<|> "config" :>+      Get '[JSON] Config+ :<|> "services" :>+      Get '[JSON] [ServiceDomain]+ :<|> "services" :>+      Capture "domain" Text :>+      Capture "service" Text :>+      ReqBody '[JSON] (Maybe Value) :>+      Post '[JSON] Value++api :: Proxy API+api = Proxy++type HA = ClientM++--------------------------------------------------------------------------------++status :: HA Value+config :: HA Config+services :: HA [ServiceDomain]++--------------------------------------------------------------------------------++-- | Represents information about the state of an entity.+data StateChange = MkStateChange {+  stateChangeAttributes :: Value,+  stateChangeEntityID :: Text,+  stateChangeLastChanged :: Text,+  stateChangeState :: Text+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "stateChange") StateChange++-- | 'callService' @domain service body@ calls @service@ in @domain@ with an+-- optional @body@. The result depends on the service.+callService :: Text -> Text -> Maybe Value -> HA Value++--------------------------------------------------------------------------------++status+  :<|> config+  :<|> services+  :<|> callService = client api++--------------------------------------------------------------------------------++authorize :: C8.ByteString -> ClientEnv -> ClientEnv+authorize token env = env{ makeClientRequest = mkRequest } where+    mkRequest url req = do+        -- Construct the request using the default function.+        baseReq <- defaultMakeClientRequest url req+        -- Then inject the Authorization header with the HA token.+        pure baseReq{+            requestHeaders =+                (hAuthorization, "Bearer " <> token) : requestHeaders baseReq+        }++mkHomeAssistantEnv :: C8.ByteString -> Manager -> BaseUrl -> ClientEnv+mkHomeAssistantEnv token httpManager address =+    authorize token $ mkClientEnv httpManager address++--------------------------------------------------------------------------------
+ src/HomeAssistant/Common/HomeAssistant.hs view
@@ -0,0 +1,31 @@+-- | Implements+module HomeAssistant.Common.HomeAssistant (+    check,+    reloadAll+) where++--------------------------------------------------------------------------------++import Data.Aeson+import qualified Data.Text as T++import HomeAssistant.Client ( HA, callService )+import HomeAssistant.Types ( Config )++--------------------------------------------------------------------------------++-- | The name of the Home Assistant service.+serviceName :: T.Text+serviceName = "homeassistant"++--------------------------------------------------------------------------------++-- | 'notify' @device notification@ is a wrapper around `callService` that uses+-- the notifications service to send @notification@ to @device@.+check :: HA (Result Config)+check = fromJSON <$> callService serviceName "check_config" Nothing++reloadAll :: HA Value+reloadAll = callService serviceName "reload_all" Nothing++--------------------------------------------------------------------------------
+ src/HomeAssistant/Common/Notify.hs view
@@ -0,0 +1,138 @@+-- | Implements+module HomeAssistant.Common.Notify (+    NotificationData(..),+    Notification,+    notification,+    setNotificationTitle,+    setNotificationData,+    notify+) where++--------------------------------------------------------------------------------++import Data.Aeson+import qualified Data.Text as T++import Deriving.Aeson++import HomeAssistant.Client++--------------------------------------------------------------------------------++-- | The name of the notifications service.+serviceName :: T.Text+serviceName = "notify"++-- | Enumerates different notification platforms that are supported by HA.+-- Different platforms have different capabilities and understand different+-- options in their payloads.+data NotificationPlatform+    -- | Generic.+    = OtherPlatform+    -- | iOS/macOS+    | Apple+    deriving (Eq, Show)++-- |+-- See https://companion.home-assistant.io/docs/notifications/actionable-notifications.+data NotificationAction = MkNotificationAction {+    -- | The text to display on the button.+    notificationActionTitle :: T.Text,+    -- | The key data to send to HA when the button is pressed.+    --+    -- Can be set to the string @"REPLY"@ to prompt for the string to return to HA.+    --+    -- On Android, this must be set to the string @"URI"@ to open the optional URI that can be specified.+    notificationActionAction :: T.Text,+    -- | Optional, the URI to open when the button is pressed.+    notificationActionURI :: Maybe T.Text,+    -- | Set to @"textInput"@ to prompt for text.+    notificationActionBehavior :: Maybe T.Text+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "notificationAction") NotificationAction++data AppleNotificationData = MkAppleNotificationData {+    -- | A URI to open when the notification is clicked on,+    -- see https://companion.home-assistant.io/docs/notifications/notifications-basic/#opening-a-url+    appleNotificationUrl :: T.Text,+    -- | A list of actions that should be available as buttons when the notification is pressed.+    appleNotificationAction :: Maybe [NotificationAction]+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "appleNotification") AppleNotificationData+++-- | Represents platform-specific data for a notification.+data NotificationData (platform :: NotificationPlatform) where+    -- | A general constructor that can be used to pass arbitrary JSON data to+    -- the notification provider.+    OtherNotificationData :: Value -> NotificationData 'OtherPlatform+    AppleNotificationData :: AppleNotificationData -> NotificationData 'Apple++instance Eq (NotificationData platform) where+    (OtherNotificationData l) == (OtherNotificationData r) = l == r+    (AppleNotificationData l) == (AppleNotificationData r) = l == r++instance Show (NotificationData platform) where+    show (OtherNotificationData d) = show d+    show (AppleNotificationData d) = show d++instance FromJSON (NotificationData OtherPlatform) where+    parseJSON = fmap OtherNotificationData . parseJSON++instance FromJSON (NotificationData Apple) where+    parseJSON = fmap AppleNotificationData . parseJSON++instance ToJSON (NotificationData platform) where+    toJSON (OtherNotificationData v) = toJSON v+    toJSON (AppleNotificationData v) = toJSON v++data Notification platform = MkNotification {+    -- | The notification message. This is required.+    notificationMessage :: T.Text,+    -- | The title of the notification. Will default to the Home Assistant app name on the+    -- target device if not set.+    notificationTitle :: Maybe T.Text,+    -- | Platform-specific data associated with the notification.+    --+    -- See the constructors of `NotificationData` for more information and the documentation+    -- at https://www.home-assistant.io/integrations/notify/+    notificationData :: Maybe (NotificationData platform),+    -- | Platform-specific target for the notification.+    notificationTarget :: Maybe Value+} deriving (Generic, Eq, Show)+  deriving (ToJSON) via CustomJSON (JSONOptions "notification") (Notification platform)++instance FromJSON (Notification OtherPlatform) where+    parseJSON = withObject "Notification" $ \v ->+        MkNotification <$> v .: "message" <*> v .: "title" <*> v .: "data" <*> v .: "target"++-- | 'notification' @message@ constructs a `Notification` containing @message@.+-- Can be used to then change other properties of the resulting `Notification`.+notification :: T.Text -> Notification platform+notification message = MkNotification{+    notificationMessage = message,+    notificationTitle = Nothing,+    notificationData = Nothing,+    notificationTarget = Nothing+}++-- | 'setNotificationTitle' @title notification@ sets @notification@'s @title@.+setNotificationTitle ::+    Maybe T.Text ->+    Notification platform ->+    Notification platform+setNotificationTitle title n = n{ notificationTitle = title }++-- | 'setNotificationData' @data notification@ sets @notification@'s @data@.+setNotificationData ::+    Maybe (NotificationData platform) ->+    Notification platform ->+    Notification platform+setNotificationData d n = n{ notificationData = d }++-- | 'notify' @device notification@ is a wrapper around `callService` that uses+-- the notifications service to send @notification@ to @device@.+notify :: T.Text -> Notification platform -> HA Value+notify device = callService serviceName device . Just . toJSON++--------------------------------------------------------------------------------
+ src/HomeAssistant/Types.hs view
@@ -0,0 +1,76 @@+-- | Types used by Home Assistant.+module HomeAssistant.Types (+    JSONOptions,+    UnitSystem(..),+    Config(..),+    Service(..),+    ServiceDomain(..)+) where++--------------------------------------------------------------------------------++import Data.Aeson+import Data.Text+import Data.Map.Lazy qualified as M++import Deriving.Aeson++import GHC.TypeLits++--------------------------------------------------------------------------------++-- | The custom JSON encoding/decoding options for HA types.+--+-- HA does not like fields that are set to @null@ - they must be omitted.+type JSONOptions (prefix :: Symbol) =+  '[OmitNothingFields, FieldLabelModifier '[StripPrefix prefix, CamelToSnake]]++--------------------------------------------------------------------------------++-- | Represents units that Home Assistant is configured to use.+data UnitSystem = MkUnitSystem {+    -- | Identifies the unit used for distances.+    unitSystemLength :: Text,+    -- | Identifies the unit used for mass.+    unitSystemMass :: Text,+    -- | Identifies the unit used for temperatures.+    unitSystemTemperature :: Text,+    -- | Identifies the unit used for volumes.+    unitSystemVolume :: Text+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "unitSystem") UnitSystem++-- | Represents Home Assistant configurations.+data Config = MkConfig {+    configComponents :: [Text],+    configConfigDir :: Text,+    configLocationName :: Text,+    configTimeZone :: Text,+    configElevation :: Int,+    configLatitude :: Double,+    configLongitude :: Double,+    -- | The units that Home Assistant is configured to use.+    configUnitSystem :: UnitSystem,+    configVersion :: Text,+    configWhitelistExternalDirs :: [Text]+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "config") Config++-- | Represents Home Assistant services.+data Service = MkService {+    serviceName :: Text,+    serviceDescription :: Text,+    serviceFields :: M.Map Text Value+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "service") Service++-- | Represents Home Assistant service domains.+data ServiceDomain = MkServiceDomain {+    -- | The domain name.+    sdDomain :: Text,+    -- | The services in this domain as a mapping from names to services.+    sdServices :: M.Map Text Service+} deriving (Generic, Eq, Show)+  deriving (FromJSON, ToJSON) via CustomJSON (JSONOptions "sd") ServiceDomain++--------------------------------------------------------------------------------
+ test/Spec.hs view
@@ -0,0 +1,2 @@+main :: IO ()+main = putStrLn "Test suite not yet implemented"