Repository: bpicode/fritzctl
Pull request #100 (2017-11-04 14:51:08)

Refactor api


Patch:
diff --git a/cmd/client.go b/cmd/client.go
index 61d9589..bf966f8 100644
--- a/cmd/client.go
+++ b/cmd/client.go
@@ -19,8 +19,9 @@ func clientLogin() *fritz.Client {
 	return client
 }
 
-func homeAutoClient() fritz.HomeAuto {
+func homeAutoClient(overrides ...fritz.Option) fritz.HomeAuto {
 	opts := findOptions(config.FindConfigFile)
+	opts = append(opts, overrides...)
 	h := fritz.NewHomeAuto(opts...)
 	err := h.Login()
 	assertNoErr(err, "login failed")
diff --git a/cmd/completion_bash.go b/cmd/completion_bash.go
index d306398..172ab5b 100644
--- a/cmd/completion_bash.go
+++ b/cmd/completion_bash.go
@@ -29,6 +29,6 @@ func init() {
 	completionCmd.AddCommand(completionBashCmd)
 }
 
-func completionBash(cmd *cobra.Command, args []string) error {
+func completionBash(_ *cobra.Command, _ []string) error {
 	return RootCmd.GenBashCompletion(os.Stdout)
 }
diff --git a/cmd/completion_zsh.go b/cmd/completion_zsh.go
index 5e38937..c369b8b 100644
--- a/cmd/completion_zsh.go
+++ b/cmd/completion_zsh.go
@@ -24,6 +24,6 @@ func init() {
 	completionCmd.AddCommand(completionZshCmd)
 }
 
-func completionZsh(cmd *cobra.Command, args []string) error {
+func completionZsh(_ *cobra.Command, _ []string) error {
 	return RootCmd.GenZshCompletion(os.Stdout)
 }
diff --git a/cmd/configure.go b/cmd/configure.go
index 4db7c81..f087a4f 100644
--- a/cmd/configure.go
+++ b/cmd/configure.go
@@ -25,7 +25,7 @@ func init() {
 	RootCmd.AddCommand(configureCmd)
 }
 
-func configure(cmd *cobra.Command, args []string) error {
+func configure(_ *cobra.Command, _ []string) error {
 	configurer := config.NewConfigurer()
 	configurer.Greet()
 	cfg, err := configurer.Obtain(configReaderSrc)
diff --git a/cmd/docs_man.go b/cmd/docs_man.go
index 8500238..f6e63e8 100644
--- a/cmd/docs_man.go
+++ b/cmd/docs_man.go
@@ -20,7 +20,7 @@ func init() {
 	docCmd.AddCommand(docManCmd)
 }
 
-func docMan(cmd *cobra.Command, args []string) error {
+func docMan(_ *cobra.Command, _ []string) error {
 	err := man.GenerateManPage(RootCmd, &man.Options{
 		Origin: man.Origin{
 			Source: "Auto-generated by bpicode/fritzctl",
diff --git a/cmd/list_groups.go b/cmd/list_groups.go
index e8a881f..876f94f 100644
--- a/cmd/list_groups.go
+++ b/cmd/list_groups.go
@@ -23,7 +23,7 @@ func init() {
 	listCmd.AddCommand(listGroupsCmd)
 }
 
-func listGroups(cmd *cobra.Command, args []string) error {
+func listGroups(_ *cobra.Command, _ []string) error {
 	c := homeAutoClient()
 	list, err := c.List()
 	assertNoErr(err, "cannot obtain data for smart home groups")
diff --git a/cmd/list_inetstats.go b/cmd/list_inetstats.go
index c4ac54e..2f72457 100644
--- a/cmd/list_inetstats.go
+++ b/cmd/list_inetstats.go
@@ -22,9 +22,9 @@ func init() {
 	listCmd.AddCommand(listInetstatsCmd)
 }
 
-func listInetstats(cmd *cobra.Command, args []string) error {
+func listInetstats(_ *cobra.Command, _ []string) error {
 	c := clientLogin()
-	f := fritz.Internal(c)
+	f := fritz.NewInternal(c)
 	stats, err := f.InternetStats()
 	assertNoErr(err, "cannot obtain internet stats")
 	logger.Success("Recent upstream/downstream time series:\n")
diff --git a/cmd/list_landevices.go b/cmd/list_landevices.go
index 685ae44..09cd8b5 100644
--- a/cmd/list_landevices.go
+++ b/cmd/list_landevices.go
@@ -21,9 +21,9 @@ func init() {
 	listCmd.AddCommand(listLanDevicesCmd)
 }
 
-func listLanDevices(cmd *cobra.Command, args []string) error {
+func listLanDevices(_ *cobra.Command, _ []string) error {
 	c := clientLogin()
-	f := fritz.Internal(c)
+	f := fritz.NewInternal(c)
 	devs, err := f.ListLanDevices()
 	assertNoErr(err, "cannot obtain LAN devices data")
 	logger.Success("Obtained LAN devices data:")
diff --git a/cmd/list_logs.go b/cmd/list_logs.go
index 0cb72c2..b796f43 100644
--- a/cmd/list_logs.go
+++ b/cmd/list_logs.go
@@ -23,9 +23,9 @@ func init() {
 	listCmd.AddCommand(listLogsCmd)
 }
 
-func listLogs(cmd *cobra.Command, args []string) error {
+func listLogs(_ *cobra.Command, _ []string) error {
 	c := clientLogin()
-	f := fritz.Internal(c)
+	f := fritz.NewInternal(c)
 	logs, err := f.ListLogs()
 	assertNoErr(err, "cannot obtain logs")
 	logger.Success("Recent log messages:")
diff --git a/cmd/list_thermostats.go b/cmd/list_thermostats.go
index eded1ce..2414ac5 100644
--- a/cmd/list_thermostats.go
+++ b/cmd/list_thermostats.go
@@ -23,7 +23,7 @@ func init() {
 	listCmd.AddCommand(listThermostatsCmd)
 }
 
-func listThermostats(cmd *cobra.Command, args []string) error {
+func listThermostats(_ *cobra.Command, _ []string) error {
 	c := homeAutoClient()
 	devs, err := c.List()
 	assertNoErr(err, "cannot obtain thermostats device data")
diff --git a/cmd/manifest_apply.go b/cmd/manifest_apply.go
index e805c41..82809b8 100644
--- a/cmd/manifest_apply.go
+++ b/cmd/manifest_apply.go
@@ -18,12 +18,12 @@ func init() {
 	manifestCmd.AddCommand(applyManifestCmd)
 }
 
-func apply(cmd *cobra.Command, args []string) error {
+func apply(_ *cobra.Command, args []string) error {
 	assertMinLen(args, 1, "insufficient input: path to input manifest expected.")
 	target := parseManifest(args[0])
-	api := fritz.HomeAutomation(clientLogin())
-	src := obtainSourcePlan(api)
-	err := manifest.AhaAPIApplier(api).Apply(src, target)
+	h := homeAutoClient(fritz.Caching(true))
+	src := obtainSourcePlan(h)
+	err := manifest.NewApplier(h).Apply(src, target)
 	assertNoErr(err, "application of manifest was not successful")
 	return nil
 }
diff --git a/cmd/manifest_export.go b/cmd/manifest_export.go
index da6f218..92bd360 100644
--- a/cmd/manifest_export.go
+++ b/cmd/manifest_export.go
@@ -3,7 +3,6 @@ package cmd
 import (
 	"os"
 
-	"github.com/bpicode/fritzctl/fritz"
 	"github.com/bpicode/fritzctl/manifest"
 	"github.com/spf13/cobra"
 )
@@ -20,10 +19,9 @@ func init() {
 	manifestCmd.AddCommand(exportManifestCmd)
 }
 
-func export(cmd *cobra.Command, args []string) error {
-	c := clientLogin()
-	f := fritz.HomeAutomation(c)
-	l, err := f.ListDevices()
+func export(_ *cobra.Command, _ []string) error {
+	h := homeAutoClient()
+	l, err := h.List()
 	assertNoErr(err, "cannot obtain device data")
 	plan := manifest.ConvertDevicelist(l)
 	manifest.ExporterTo(os.Stdout).Export(plan)
diff --git a/cmd/manifest_plan.go b/cmd/manifest_plan.go
index 3166aac..f2e5397 100644
--- a/cmd/manifest_plan.go
+++ b/cmd/manifest_plan.go
@@ -1,7 +1,6 @@
 package cmd
 
 import (
-	"github.com/bpicode/fritzctl/fritz"
 	"github.com/bpicode/fritzctl/manifest"
 	"github.com/spf13/cobra"
 )
@@ -18,11 +17,11 @@ func init() {
 	manifestCmd.AddCommand(planManifestCmd)
 }
 
-func plan(cmd *cobra.Command, args []string) error {
+func plan(_ *cobra.Command, args []string) error {
 	assertMinLen(args, 1, "insufficient input: path to input manifest expected.")
 	target := parseManifest(args[0])
-	api := fritz.HomeAutomation(clientLogin())
-	src := obtainSourcePlan(api)
+	h := homeAutoClient()
+	src := obtainSourcePlan(h)
 	err := manifest.DryRunner().Apply(src, target)
 	assertNoErr(err, "plan (dry-run) of manifest was not successful")
 	return nil
diff --git a/cmd/manifest_util.go b/cmd/manifest_util.go
index decfe4c..4c0ed91 100644
--- a/cmd/manifest_util.go
+++ b/cmd/manifest_util.go
@@ -16,8 +16,8 @@ func parseManifest(filename string) *manifest.Plan {
 	return p
 }
 
-func obtainSourcePlan(api fritz.HomeAutomationAPI) *manifest.Plan {
-	l, err := api.ListDevices()
+func obtainSourcePlan(h fritz.HomeAuto) *manifest.Plan {
+	l, err := h.List()
 	assertNoErr(err, "cannot obtain device data")
 	return manifest.ConvertDevicelist(l)
 }
diff --git a/cmd/ping.go b/cmd/ping.go
index 0a070a7..1fd471d 100644
--- a/cmd/ping.go
+++ b/cmd/ping.go
@@ -17,7 +17,7 @@ func init() {
 	RootCmd.AddCommand(pingCmd)
 }
 
-func ping(cmd *cobra.Command, args []string) error {
+func ping(_ *cobra.Command, _ []string) error {
 	clientLogin()
 	logger.Success("Success! FRITZ!Box seems to be alive!")
 	return nil
diff --git a/cmd/root.go b/cmd/root.go
index 2d6a031..5ff23db 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -16,7 +16,7 @@ var RootCmd = &cobra.Command{
 
 func init() {
 	cobra.OnInitialize()
-	RootCmd.PersistentFlags().Var(&logger.Loglevel{}, "loglevel", "logging verbosity")
+	RootCmd.PersistentFlags().Var(&logger.Level{}, "loglevel", "logging verbosity")
 	RootCmd.InitDefaultHelpFlag()
 	RootCmd.InitDefaultHelpCmd()
 }
diff --git a/cmd/sessionid.go b/cmd/sessionid.go
index 06f80ee..fe5a254 100644
--- a/cmd/sessionid.go
+++ b/cmd/sessionid.go
@@ -18,7 +18,7 @@ func init() {
 	RootCmd.AddCommand(sessionIDCmd)
 }
 
-func sessionID(cmd *cobra.Command, args []string) error {
+func sessionID(_ *cobra.Command, _ []string) error {
 	client := clientLogin()
 	logger.Success("Successfully obtained session ID:", client.SessionInfo.SID)
 	return nil
diff --git a/cmd/switch_off.go b/cmd/switch_off.go
index 7a39cb5..1b9f86d 100644
--- a/cmd/switch_off.go
+++ b/cmd/switch_off.go
@@ -17,7 +17,7 @@ func init() {
 	switchCmd.AddCommand(switchOffCmd)
 }
 
-func switchOff(cmd *cobra.Command, args []string) error {
+func switchOff(_ *cobra.Command, args []string) error {
 	assertMinLen(args, 1, "insufficient input: device/group name(s) expected.")
 	c := homeAutoClient()
 	err := c.Off(args...)
diff --git a/cmd/switch_on.go b/cmd/switch_on.go
index 0d55b0a..468a7f5 100644
--- a/cmd/switch_on.go
+++ b/cmd/switch_on.go
@@ -17,7 +17,7 @@ func init() {
 	switchCmd.AddCommand(switchOnCmd)
 }
 
-func switchOn(cmd *cobra.Command, args []string) error {
+func switchOn(_ *cobra.Command, args []string) error {
 	assertMinLen(args, 1, "insufficient input: device name(s) expected")
 	c := homeAutoClient()
 	err := c.On(args...)
diff --git a/cmd/version.go b/cmd/version.go
index 0d56c48..02bc78b 100644
--- a/cmd/version.go
+++ b/cmd/version.go
@@ -18,7 +18,7 @@ func init() {
 	RootCmd.AddCommand(versionCmd)
 }
 
-func printVersion(cmd *cobra.Command, args []string) error {
+func printVersion(_ *cobra.Command, _ []string) error {
 	fmt.Println(config.Version)
 	return nil
 }
diff --git a/fritz/aha.go b/fritz/aha.go
deleted file mode 100644
index 9b4f5e0..0000000
--- a/fritz/aha.go
+++ /dev/null
@@ -1,115 +0,0 @@
-package fritz
-
-import (
-	"fmt"
-	"net/http"
-
-	"github.com/bpicode/fritzctl/httpread"
-	"github.com/bpicode/fritzctl/logger"
-)
-
-// HomeAutomationAPI API definition, guided by
-// https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf.
-type HomeAutomationAPI interface {
-	ListDevices() (*Devicelist, error)
-	NameToAinTable() (map[string]string, error)
-	SwitchOn(ain string) (string, error)
-	SwitchOff(ain string) (string, error)
-	Toggle(ain string) (string, error)
-	ApplyTemperature(value float64, ain string) (string, error)
-}
-
-// HomeAutomation creates a Fritz AHA API from a given client.
-func HomeAutomation(client *Client) HomeAutomationAPI {
-	return &ahaHTTP{client: client}
-}
-
-type ahaHTTP struct {
-	client *Client
-}
-
-func (aha *ahaHTTP) getf(url string) func() (*http.Response, error) {
-	return func() (*http.Response, error) {
-		logger.Debug("GET", url)
-		return aha.client.HTTPClient.Get(url)
-	}
-}
-
-// ListDevices lists the basic data of the smart home devices.
-func (aha *ahaHTTP) ListDevices() (*Devicelist, error) {
-	url := aha.homeAutoSwitch().
-		query("switchcmd", "getdevicelistinfos").
-		build()
-	var deviceList Devicelist
-	errRead := httpread.XML(aha.getf(url), &deviceList)
-	return &deviceList, errRead
-}
-
-// SwitchOn switches a device on. The device is identified by its AIN.
-func (aha *ahaHTTP) SwitchOn(ain string) (string, error) {
-	return aha.switchForAin(ain, "setswitchon")
-}
-
-// SwitchOff switches a device off. The device is identified by its AIN.
-func (aha *ahaHTTP) SwitchOff(ain string) (string, error) {
-	return aha.switchForAin(ain, "setswitchoff")
-}
-
-// Toggle toggles the on/off state of a device. The device is identified by its AIN.
-func (aha *ahaHTTP) Toggle(ain string) (string, error) {
-	url := aha.homeAutoSwitch().
-		query("ain", ain).
-		query("switchcmd", "setswitchtoggle").
-		build()
-	return httpread.String(aha.getf(url))
-}
-
-// ApplyTemperature sets the desired temperature on a "HKR" device. The device is identified by its AIN.
-func (aha *ahaHTTP) ApplyTemperature(value float64, ain string) (string, error) {
-	param, err := temperatureParam(value)
-	if err != nil {
-		return "", err
-	}
-	url := aha.homeAutoSwitch().
-		query("ain", ain).
-		query("switchcmd", "sethkrtsoll").
-		query("param", fmt.Sprintf("%d", param)).
-		build()
-	return httpread.String(aha.getf(url))
-}
-
-func (aha *ahaHTTP) switchForAin(ain, command string) (string, error) {
-	url := aha.homeAutoSwitch().
-		query("ain", ain).
-		query("switchcmd", command).
-		build()
-	return httpread.String(aha.getf(url))
-}
-
-// NameToAinTable returns a lookup name -> AIN.
-func (aha *ahaHTTP) NameToAinTable() (map[string]string, error) {
-	devList, err := aha.ListDevices()
-	if err != nil {
-		return nil, err
-	}
-	return devList.NamesAndAins(), nil
-}
-
-func (aha *ahaHTTP) homeAutoSwitch() fritzURLBuilder {
-	return newURLBuilder(aha.client.Config).path(homeAutomationURI).query("sid", aha.client.SessionInfo.SID)
-}
-
-func temperatureParam(t float64) (int64, error) {
-	doubled := round(2 * t)
-	regular := doubled >= 16 && doubled <= 56
-	special := doubled == 253 || doubled == 254
-	if !(regular || special) {
-		return 0, fmt.Errorf("invalid temperature value: %.1f°C is not contained in the set of acceptable values: 8-28°C, 126.5, 127", t)
-	}
-	return doubled, nil
-}
-
-// round rounds a float64 value to an integer.
-func round(v float64) int64 {
-	return int64(v + 0.5)
-}
diff --git a/fritz/aha_concurrent.go b/fritz/aha_concurrent.go
deleted file mode 100644
index a5e75f9..0000000
--- a/fritz/aha_concurrent.go
+++ /dev/null
@@ -1,120 +0,0 @@
-package fritz
-
-import (
-	"strings"
-
-	"github.com/bpicode/fritzctl/logger"
-	"github.com/bpicode/fritzctl/stringutils"
-	"github.com/pkg/errors"
-)
-
-// homeAutoConfigurator allows to reconfigure AHA systems.
-type homeAutoConfigurator interface {
-	on(names ...string) error
-	off(names ...string) error
-	toggle(names ...string) error
-	temp(value float64, names ...string) error
-}
-
-// concurrentConfigurator creates a Fritz AHA API from a given base API an applies commands in parallel using the
-// go concurrency programming model.
-func concurrentConfigurator(homeAuto HomeAutomationAPI) homeAutoConfigurator {
-	return &concurrentAhaHTTP{homeAuto: homeAuto}
-}
-
-type concurrentAhaHTTP struct {
-	homeAuto HomeAutomationAPI
-}
-
-// temp sets the desired temperature of "HKR" devices.
-func (aha *concurrentAhaHTTP) temp(value float64, names ...string) error {
-	return aha.doConcurrently(func(ain string) func() (string, error) {
-		return func() (string, error) {
-			return aha.homeAuto.ApplyTemperature(value, ain)
-		}
-	}, names...)
-}
-
-// on switches devices on. The devices are identified by their names.
-func (aha *concurrentAhaHTTP) on(names ...string) error {
-	return aha.doConcurrently(func(ain string) func() (string, error) {
-		return func() (string, error) {
-			return aha.homeAuto.SwitchOn(ain)
-		}
-	}, names...)
-}
-
-// off switches devices off. The devices are identified by their names.
-func (aha *concurrentAhaHTTP) off(names ...string) error {
-	return aha.doConcurrently(func(ain string) func() (string, error) {
-		return func() (string, error) {
-			return aha.homeAuto.SwitchOff(ain)
-		}
-	}, names...)
-}
-
-// toggle toggles the on/off state of devices.
-func (aha *concurrentAhaHTTP) toggle(names ...string) error {
-	return aha.doConcurrently(func(ain string) func() (string, error) {
-		return func() (string, error) {
-			return aha.homeAuto.Toggle(ain)
-		}
-	}, names...)
-}
-
-func (aha *concurrentAhaHTTP) doConcurrently(workFactory func(string) func() (string, error), names ...string) error {
-	targets, err := buildBacklog(aha.homeAuto, names, workFactory)
-	if err != nil {
-		return err
-	}
-	results := scatterGather(targets, genericSuccessHandler, genericErrorHandler)
-	return genericResult(results)
-}
-
-func genericSuccessHandler(key, message string) result {
-	logger.Success("Successfully processed '" + key + "'; response was: " + strings.TrimSpace(message))
-	return result{msg: message, err: nil}
-}
-
-func genericErrorHandler(key, message string, err error) result {
-	logger.Warn("Error while processing '" + key + "'; error was: " + err.Error())
-	return result{msg: message, err: errors.Wrapf(err, "error operating '%s'", key)}
-}
-
-func genericResult(results []result) error {
-	if err := truncateToOne(results); err != nil {
-		return errors.Wrap(err, "not all operations could be completed")
-	}
-	return nil
-}
-
-func truncateToOne(results []result) error {
-	errs := make([]error, 0, len(results))
-	for _, res := range results {
-		if res.err != nil {
-			errs = append(errs, res.err)
-		}
-	}
-	if len(errs) > 0 {
-		msgs := stringutils.ErrorMessages(errs)
-		return errors.New(strings.Join(msgs, "; "))
-	}
-	return nil
-}
-
-func buildBacklog(aha HomeAutomationAPI, names []string, workFactory func(string) func() (string, error)) (map[string]func() (string, error), error) {
-	namesAndAins, err := aha.NameToAinTable()
-	if err != nil {
-		return nil, err
-	}
-	targets := make(map[string]func() (string, error))
-	for _, name := range names {
-		ain, ok := namesAndAins[name]
-		if ain == "" || !ok {
-			quoted := stringutils.Quote(stringutils.StringKeys(namesAndAins))
-			return nil, errors.Errorf("nothing found with name '%s'; choose one out of '%s'", name, strings.Join(quoted, ", "))
-		}
-		targets[name] = workFactory(ain)
-	}
-	return targets, nil
-}
diff --git a/fritz/ain_based.go b/fritz/ain_based.go
new file mode 100644
index 0000000..eafbc26
--- /dev/null
+++ b/fritz/ain_based.go
@@ -0,0 +1,105 @@
+package fritz
+
+import (
+	"fmt"
+	"net/http"
+
+	"github.com/bpicode/fritzctl/httpread"
+	"github.com/bpicode/fritzctl/logger"
+)
+
+// ainBased API definition, guided by
+// https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf.
+type ainBased interface {
+	listDevices() (*Devicelist, error)
+	switchOn(ain string) (string, error)
+	switchOff(ain string) (string, error)
+	toggle(ain string) (string, error)
+	applyTemperature(value float64, ain string) (string, error)
+}
+
+// newAinBased creates a Fritz AHA API (working on AINs) from a given client.
+func newAinBased(client *Client) ainBased {
+	return &ainBasedClient{client: client}
+}
+
+type ainBasedClient struct {
+	client *Client
+}
+
+func (a *ainBasedClient) getf(url string) func() (*http.Response, error) {
+	return func() (*http.Response, error) {
+		logger.Debug("GET", url)
+		return a.client.HTTPClient.Get(url)
+	}
+}
+
+// listDevices lists the basic data of the smart home devices.
+func (a *ainBasedClient) listDevices() (*Devicelist, error) {
+	url := a.homeAutoSwitch().
+		query("switchcmd", "getdevicelistinfos").
+		build()
+	var deviceList Devicelist
+	errRead := httpread.XML(a.getf(url), &deviceList)
+	return &deviceList, errRead
+}
+
+// switchOn switches a device on. The device is identified by its AIN.
+func (a *ainBasedClient) switchOn(ain string) (string, error) {
+	return a.switchForAin(ain, "setswitchon")
+}
+
+// switchOff switches a device off. The device is identified by its AIN.
+func (a *ainBasedClient) switchOff(ain string) (string, error) {
+	return a.switchForAin(ain, "setswitchoff")
+}
+
+// toggle toggles the on/off state of a device. The device is identified by its AIN.
+func (a *ainBasedClient) toggle(ain string) (string, error) {
+	url := a.homeAutoSwitch().
+		query("ain", ain).
+		query("switchcmd", "setswitchtoggle").
+		build()
+	return httpread.String(a.getf(url))
+}
+
+// applyTemperature sets the desired temperature on a "HKR" device. The device is identified by its AIN.
+func (a *ainBasedClient) applyTemperature(value float64, ain string) (string, error) {
+	param, err := temperatureParam(value)
+	if err != nil {
+		return "", err
+	}
+	url := a.homeAutoSwitch().
+		query("ain", ain).
+		query("switchcmd", "sethkrtsoll").
+		query("param", fmt.Sprintf("%d", param)).
+		build()
+	return httpread.String(a.getf(url))
+}
+
+func (a *ainBasedClient) switchForAin(ain, command string) (string, error) {
+	url := a.homeAutoSwitch().
+		query("ain", ain).
+		query("switchcmd", command).
+		build()
+	return httpread.String(a.getf(url))
+}
+
+func (a *ainBasedClient) homeAutoSwitch() fritzURLBuilder {
+	return newURLBuilder(a.client.Config).path(homeAutomationURI).query("sid", a.client.SessionInfo.SID)
+}
+
+func temperatureParam(t float64) (int64, error) {
+	doubled := round(2 * t)
+	regular := doubled >= 16 && doubled <= 56
+	special := doubled == 253 || doubled == 254
+	if !(regular || special) {
+		return 0, fmt.Errorf("invalid temperature value: %.1f°C is not contained in the set of acceptable values: 8-28°C, 126.5, 127", t)
+	}
+	return doubled, nil
+}
+
+// round rounds a float64 value to an integer.
+func round(v float64) int64 {
+	return int64(v + 0.5)
+}
diff --git a/fritz/api_aha.go b/fritz/api_aha.go
new file mode 100644
index 0000000..a8c8377
--- /dev/null
+++ b/fritz/api_aha.go
@@ -0,0 +1,168 @@
+package fritz
+
+import (
+	"strings"
+	"sync"
+
+	"github.com/bpicode/fritzctl/logger"
+	"github.com/bpicode/fritzctl/stringutils"
+	"github.com/pkg/errors"
+)
+
+// HomeAuto is a client for the Home Automation HTTP Interface,
+// see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf.
+type HomeAuto interface {
+	Login() error
+	List() (*Devicelist, error)
+	On(names ...string) error
+	Off(names ...string) error
+	Toggle(names ...string) error
+	Temp(value float64, names ...string) error
+}
+
+// NewHomeAuto a HomeAuto that communicates with the FRITZ!Box by means of the Home Automation HTTP Interface.
+func NewHomeAuto(options ...Option) HomeAuto {
+	client := defaultClient()
+	aha := newAinBased(client)
+	homeAuto := homeAuto{
+		client:  client,
+		aha:     aha,
+		caching: false,
+	}
+	for _, option := range options {
+		option(&homeAuto)
+	}
+	return &homeAuto
+}
+
+// Option applies fine-grained configuration to the HomeAuto client.
+type Option func(h *homeAuto)
+
+// codebeat:disable[TOO_MANY_IVARS]
+type homeAuto struct {
+	client        *Client
+	aha           ainBased
+	caching       bool
+	cacheLock     sync.Mutex
+	cachedDevices *Devicelist
+}
+
+// codebeat:enable[TOO_MANY_IVARS]
+
+// Login tries to authenticate against the FRITZ!Box. If not successful, an error is returned. This method should be
+// called before any of the other methods unless authentication is turned off at the FRITZ!Box itself.
+func (h *homeAuto) Login() error {
+	return h.client.Login()
+}
+
+// List fetches the devices known at the FRITZ!Box. See Devicelist for details. If the devices could not be obtained,
+// an error is returned.
+func (h *homeAuto) List() (*Devicelist, error) {
+	h.cacheLock.Lock()
+	defer h.cacheLock.Unlock()
+	if h.caching && h.cachedDevices != nil {
+		logger.Debug("Device list cache hit")
+		l := *h.cachedDevices
+		return &l, nil
+	}
+	l, err := h.aha.listDevices()
+	if h.caching {
+		h.cachedDevices = l
+	}
+	return l, err
+}
+
+// On activates the given devices. Devices are identified by their name. If any of the operations does not succeed,
+// an error is returned.
+func (h *homeAuto) On(names ...string) error {
+	return h.doConcurrently(func(ain string) func() (string, error) {
+		return func() (string, error) {
+			return h.aha.switchOn(ain)
+		}
+	}, names...)
+}
+
+// Off deactivates the given devices. Devices are identified by their name. Inverse of On.
+func (h *homeAuto) Off(names ...string) error {
+	return h.doConcurrently(func(ain string) func() (string, error) {
+		return func() (string, error) {
+			return h.aha.switchOff(ain)
+		}
+	}, names...)
+}
+
+// toggle switches the state of the given devices from ON to OFF and vice versa. Devices are identified by their name.
+func (h *homeAuto) Toggle(names ...string) error {
+	return h.doConcurrently(func(ain string) func() (string, error) {
+		return func() (string, error) {
+			return h.aha.toggle(ain)
+		}
+	}, names...)
+}
+
+// Temp applies the temperature setting to the given devices. Devices are identified by their name.
+func (h *homeAuto) Temp(value float64, names ...string) error {
+	return h.doConcurrently(func(ain string) func() (string, error) {
+		return func() (string, error) {
+			return h.aha.applyTemperature(value, ain)
+		}
+	}, names...)
+}
+
+func (h *homeAuto) doConcurrently(workFactory func(string) func() (string, error), names ...string) error {
+	targets, err := buildBacklog(h, names, workFactory)
+	if err != nil {
+		return err
+	}
+	results := scatterGather(targets, genericSuccessHandler, genericErrorHandler)
+	return genericResult(results)
+}
+
+func genericSuccessHandler(key, message string) result {
+	logger.Success("Successfully processed '" + key + "'; response was: " + strings.TrimSpace(message))
+	return result{msg: message, err: nil}
+}
+
+func genericErrorHandler(key, message string, err error) result {
+	logger.Warn("Error while processing '" + key + "'; error was: " + err.Error())
+	return result{msg: message, err: errors.Wrapf(err, "error operating '%s'", key)}
+}
+
+func genericResult(results []result) error {
+	if err := truncateToOne(results); err != nil {
+		return errors.Wrap(err, "not all operations could be completed")
+	}
+	return nil
+}
+
+func truncateToOne(results []result) error {
+	errs := make([]error, 0, len(results))
+	for _, res := range results {
+		if res.err != nil {
+			errs = append(errs, res.err)
+		}
+	}
+	if len(errs) > 0 {
+		msgs := stringutils.ErrorMessages(errs)
+		return errors.New(strings.Join(msgs, "; "))
+	}
+	return nil
+}
+
+func buildBacklog(h HomeAuto, names []string, workFactory func(string) func() (string, error)) (map[string]func() (string, error), error) {
+	devList, err := h.List()
+	if err != nil {
+		return nil, errors.Wrap(err, "unable to list devices")
+	}
+	namesAndAins := devList.NamesAndAins()
+	targets := make(map[string]func() (string, error))
+	for _, name := range names {
+		ain, ok := namesAndAins[name]
+		if ain == "" || !ok {
+			quoted := stringutils.Quote(stringutils.StringKeys(namesAndAins))
+			return nil, errors.Errorf("nothing found with name '%s'; choose one out of '%s'", name, strings.Join(quoted, ", "))
+		}
+		targets[name] = workFactory(ain)
+	}
+	return targets, nil
+}
diff --git a/fritz/api.go b/fritz/api_aha_options.go
similarity index 53%
rename from fritz/api.go
rename to fritz/api_aha_options.go
index 692f857..d274a55 100644
--- a/fritz/api.go
+++ b/fritz/api_aha_options.go
@@ -10,75 +10,6 @@ import (
 	"github.com/bpicode/fritzctl/logger"
 )
 
-// HomeAuto is a client for the Home Automation HTTP Interface,
-// see https://avm.de/fileadmin/user_upload/Global/Service/Schnittstellen/AHA-HTTP-Interface.pdf.
-type HomeAuto interface {
-	Login() error
-	List() (*Devicelist, error)
-	On(names ...string) error
-	Off(names ...string) error
-	Toggle(names ...string) error
-	Temp(value float64, names ...string) error
-}
-
-type homeAuto struct {
-	client *Client
-	aha    HomeAutomationAPI
-	cAha   homeAutoConfigurator
-}
-
-// Login tries to authenticate against the FRITZ!Box. If not successful, an error is returned. This method should be
-// called before any of the other methods unless authentication is turned off at the FRITZ!Box itself.
-func (h *homeAuto) Login() error {
-	return h.client.Login()
-}
-
-// List fetches the devices known at the FRITZ!Box. See Devicelist for details. If the devices could not be obtained,
-// an error is returned.
-func (h *homeAuto) List() (*Devicelist, error) {
-	return h.aha.ListDevices()
-}
-
-// On activates the given devices. Devices are identified by their name. If any of the operations does not succeed,
-// an error is returned.
-func (h *homeAuto) On(names ...string) error {
-	return h.cAha.on(names...)
-}
-
-// Off deactivates the given devices. Devices are identified by their name. Inverse of On.
-func (h *homeAuto) Off(names ...string) error {
-	return h.cAha.off(names...)
-}
-
-// Toggle switches the state of the given devices from ON to OFF and vice versa. Devices are identified by their name.
-func (h *homeAuto) Toggle(names ...string) error {
-	return h.cAha.toggle(names...)
-}
-
-// Temp applies the temperature setting to the given devices. Devices are identified by their name.
-func (h *homeAuto) Temp(value float64, names ...string) error {
-	return h.cAha.temp(value, names...)
-}
-
-// Option applies fine-grained configuration to the HomeAuto client.
-type Option func(h *homeAuto)
-
-// NewHomeAuto a HomeAuto that communicates with the FRITZ!Box by means of the Home Automation HTTP Interface.
-func NewHomeAuto(options ...Option) HomeAuto {
-	client := defaultClient()
-	aha := HomeAutomation(client)
-	cAha := concurrentConfigurator(aha)
-	homeAuto := homeAuto{
-		client: client,
-		aha:    aha,
-		cAha:   cAha,
-	}
-	for _, option := range options {
-		option(&homeAuto)
-	}
-	return &homeAuto
-}
-
 // URL sets the target host of the FRITZ!Box. Note that for usual setups, the url https://fritz.box:443 works.
 func URL(u *url.URL) Option {
 	return func(h *homeAuto) {
@@ -127,6 +58,13 @@ func AuthEndpoint(s string) Option {
 	}
 }
 
+// Caching activates (or deactivates) caching of obtained data, such as the list of devices.
+func Caching(b bool) Option {
+	return func(h *homeAuto) {
+		h.caching = b
+	}
+}
+
 func defaultClient() *Client {
 	return &Client{
 		Config:      defaultConfig(),
diff --git a/fritz/api_internal.go b/fritz/api_internal.go
new file mode 100644
index 0000000..850ecfc
--- /dev/null
+++ b/fritz/api_internal.go
@@ -0,0 +1,73 @@
+package fritz
+
+import (
+	"net/http"
+
+	"github.com/bpicode/fritzctl/httpread"
+	"github.com/bpicode/fritzctl/logger"
+)
+
+// Internal exposes Fritz!Box internal and undocumented API.
+type Internal interface {
+	ListLanDevices() (*LanDevices, error)
+	ListLogs() (*MessageLog, error)
+	InternetStats() (*TrafficMonitoringData, error)
+}
+
+// NewInternal creates a Fritz/internal API from a given client.
+func NewInternal(client *Client) Internal {
+	return &internal{client: client}
+}
+
+type internal struct {
+	client *Client
+}
+
+// ListLogs lists the log statements produced by the FRITZ!Box.
+func (i *internal) ListLogs() (*MessageLog, error) {
+	url := i.
+		query().
+		query("mq_log", "logger:status/log").
+		build()
+	var logs MessageLog
+	err := httpread.JSON(i.getf(url), &logs)
+	return &logs, err
+}
+
+// ListLanDevices lists the basic data of the LAN devices.
+func (i *internal) ListLanDevices() (*LanDevices, error) {
+	url := i.
+		query().
+		query("network", "landevice:settings/landevice/list(name,ip,mac,UID,dhcp,wlan,ethernet,active,wakeup,deleteable,source,online,speed,guest,url)").
+		build()
+	var devs LanDevices
+	err := httpread.JSON(i.getf(url), &devs)
+	return &devs, err
+}
+
+// InternetStats up/downstream statistics reported by the FRITZ!Box.
+func (i *internal) InternetStats() (*TrafficMonitoringData, error) {
+	url := i.
+		inetStat().
+		query("useajax", "1").
+		query("action", "get_graphic").
+		build()
+	var data []TrafficMonitoringData
+	err := httpread.JSON(i.getf(url), &data)
+	return &data[0], err
+}
+
+func (i *internal) query() fritzURLBuilder {
+	return newURLBuilder(i.client.Config).path(queryURI).query("sid", i.client.SessionInfo.SID)
+}
+
+func (i *internal) inetStat() fritzURLBuilder {
+	return newURLBuilder(i.client.Config).path(inetStatURI).query("sid", i.client.SessionInfo.SID)
+}
+
+func (i *internal) getf(url string) func() (*http.Response, error) {
+	return func() (*http.Response, error) {
+		logger.Debug("GET", url)
+		return i.client.HTTPClient.Get(url)
+	}
+}
diff --git a/fritz/fritzclient.go b/fritz/fritzclient.go
index baac34c..93f63a6 100644
--- a/fritz/fritzclient.go
+++ b/fritz/fritzclient.go
@@ -18,10 +18,9 @@ import (
 
 // Client encapsulates the FRITZ!Box interaction API.
 type Client struct {
-	Config      *config.Config  // The client configuration.
-	transport   *http.Transport // HTTP transport settings.
-	HTTPClient  *http.Client    // The HTTP client.
-	SessionInfo *SessionInfo    // The current session data of the client.
+	Config      *config.Config // The client configuration.
+	HTTPClient  *http.Client   // The HTTP client.
+	SessionInfo *SessionInfo   // The current session data of the client.
 }
 
 // SessionInfo models the xml upon accessing the login endpoint.
@@ -39,7 +38,7 @@ func NewClient(configfile string) (*Client, error) {
 	tlsConfig := tlsConfigFrom(configPtr)
 	transport := &http.Transport{TLSClientConfig: tlsConfig}
 	httpClient := &http.Client{Transport: transport}
-	return &Client{Config: configPtr, transport: transport, HTTPClient: httpClient}, nil
+	return &Client{Config: configPtr, HTTPClient: httpClient}, nil
 }
 
 // Login tries to login into the box and obtain the session id.
diff --git a/fritz/internal.go b/fritz/internal.go
deleted file mode 100644
index 96d545d..0000000
--- a/fritz/internal.go
+++ /dev/null
@@ -1,73 +0,0 @@
-package fritz
-
-import (
-	"net/http"
-
-	"github.com/bpicode/fritzctl/httpread"
-	"github.com/bpicode/fritzctl/logger"
-)
-
-// InternalAPI exposes Fritz!Box internal and undocumented API.
-type InternalAPI interface {
-	ListLanDevices() (*LanDevices, error)
-	ListLogs() (*MessageLog, error)
-	InternetStats() (*TrafficMonitoringData, error)
-}
-
-// Internal creates a Fritz/internal API from a given client.
-func Internal(client *Client) InternalAPI {
-	return &internalHTTP{client: client}
-}
-
-type internalHTTP struct {
-	client *Client
-}
-
-// ListLogs lists the log statements produced by the FRITZ!Box.
-func (internal *internalHTTP) ListLogs() (*MessageLog, error) {
-	url := internal.
-		query().
-		query("mq_log", "logger:status/log").
-		build()
-	var logs MessageLog
-	err := httpread.JSON(internal.getf(url), &logs)
-	return &logs, err
-}
-
-// ListLanDevices lists the basic data of the LAN devices.
-func (internal *internalHTTP) ListLanDevices() (*LanDevices, error) {
-	url := internal.
-		query().
-		query("network", "landevice:settings/landevice/list(name,ip,mac,UID,dhcp,wlan,ethernet,active,wakeup,deleteable,source,online,speed,guest,url)").
-		build()
-	var devs LanDevices
-	errRead := httpread.JSON(internal.getf(url), &devs)
-	return &devs, errRead
-}
-
-// InternetStats up/downstream statistics reported by the FRITZ!Box.
-func (internal *internalHTTP) InternetStats() (*TrafficMonitoringData, error) {
-	url := internal.
-		inetStat().
-		query("useajax", "1").
-		query("action", "get_graphic").
-		build()
-	var data []TrafficMonitoringData
-	err := httpread.JSON(internal.getf(url), &data)
-	return &data[0], err
-}
-
-func (internal *internalHTTP) query() fritzURLBuilder {
-	return newURLBuilder(internal.client.Config).path(queryURI).query("sid", internal.client.SessionInfo.SID)
-}
-
-func (internal *internalHTTP) inetStat() fritzURLBuilder {
-	return newURLBuilder(internal.client.Config).path(inetStatURI).query("sid", internal.client.SessionInfo.SID)
-}
-
-func (internal *internalHTTP) getf(url string) func() (*http.Response, error) {
-	return func() (*http.Response, error) {
-		logger.Debug("GET", url)
-		return internal.client.HTTPClient.Get(url)
-	}
-}
diff --git a/logger/doc.go b/logger/doc.go
new file mode 100644
index 0000000..8bec8dd
--- /dev/null
+++ b/logger/doc.go
@@ -0,0 +1,3 @@
+// Package logger provides functions to log on different levels/severities. Levels have a canonical ordering and
+// there is support silencing log statements with levels below a given threshold.
+package logger
diff --git a/logger/flags.go b/logger/flags.go
index 3bcd57b..e4eb4e2 100644
--- a/logger/flags.go
+++ b/logger/flags.go
@@ -4,22 +4,22 @@ import (
 	"github.com/pkg/errors"
 )
 
-// Loglevel represents a Value for different logging configs.
-type Loglevel struct {
+// Level represents a Value for different logging configs.
+type Level struct {
 }
 
 // Type returns a name for the value type.
-func (l *Loglevel) Type() string {
+func (l *Level) Type() string {
 	return "string"
 }
 
 // String represents the default value.
-func (l *Loglevel) String() string {
+func (l *Level) String() string {
 	return "info"
 }
 
 // Set configures the loglevel for the application.
-func (l *Loglevel) Set(val string) error {
+func (l *Level) Set(val string) error {
 	err := configureLogLevel(val)
 	if err != nil {
 		return errors.Wrapf(err, "cannot apply loglevel configuration for value '%s'", val)
diff --git a/logger/loglevel.go b/logger/loglevel.go
index c258ede..ae31109 100644
--- a/logger/loglevel.go
+++ b/logger/loglevel.go
@@ -71,7 +71,7 @@ func byName(name string) (*printers, error) {
 }
 
 func (l *levelLookupTable) keys() []string {
-	keys := []string{}
+	var keys []string
 	for k := range *l {
 		keys = append(keys, k)
 	}
diff --git a/manifest/applier_aha_http.go b/manifest/applier_aha_http.go
index 87ea12e..d1b9995 100644
--- a/manifest/applier_aha_http.go
+++ b/manifest/applier_aha_http.go
@@ -14,13 +14,13 @@ type ahaAPIApplier struct {
 }
 
 type aha interface {
-	SwitchOn(ain string) (string, error)
-	SwitchOff(ain string) (string, error)
-	ApplyTemperature(value float64, ain string) (string, error)
+	On(names ...string) error
+	Off(names ...string) error
+	Temp(value float64, names ...string) error
 }
 
-// AhaAPIApplier is an Applier that performs changes to the AHA system via the HTTP API.
-func AhaAPIApplier(f aha) Applier {
+// NewApplier is an Applier that performs changes to the AHA system via the HTTP API.
+func NewApplier(f aha) Applier {
 	return &ahaAPIApplier{fritz: f}
 }
 
@@ -89,9 +89,9 @@ func reconfigureSwitch(before, after Switch) Action {
 func (a *reconfigureSwitchAction) Perform(f aha) (err error) {
 	if a.before.State != a.after.State {
 		if a.after.State {
-			_, err = f.SwitchOn(a.before.ain)
+			err = f.On(a.before.Name)
 		} else {
-			_, err = f.SwitchOff(a.before.ain)
+			err = f.Off(a.before.Name)
 		}
 		if err == nil {
 			fmt.Printf("\t[%s]\t'%s'\t%s\t⟶\t%s\n", console.Green("OK"), a.before.Name, console.Btoc(a.before.State), console.Btoc(a.after.State))
@@ -114,7 +114,7 @@ func reconfigureThermostat(before, after Thermostat) Action {
 // Perform applies the target state to a switch by turning it on/off.
 func (a *reconfigureThermostatAction) Perform(f aha) (err error) {
 	if a.before.Temperature != a.after.Temperature {
-		_, err = f.ApplyTemperature(a.after.Temperature, a.before.ain)
+		err = f.Temp(a.after.Temperature, a.before.Name)
 		if err == nil {
 			fmt.Printf("\t[%s]\t'%s'\t%.1f°C\t⟶\t%.1f°C\n", console.Green("OK"), a.before.Name, a.before.Temperature, a.after.Temperature)
 		} else {
diff --git a/manifest/convert.go b/manifest/convert.go
index e5fe269..30272c3 100644
--- a/manifest/convert.go
+++ b/manifest/convert.go
@@ -22,7 +22,6 @@ func convertSwitch(d *fritz.Device) Switch {
 	var s Switch
 	s.Name = d.Name
 	s.State, _ = strconv.ParseBool(d.Switch.State)
-	s.ain = d.Identifier
 	return s
 }
 
@@ -31,6 +30,5 @@ func convertThermostat(d *fritz.Device) Thermostat {
 	t.Name = d.Name
 	goalTimesTwo, _ := strconv.ParseFloat(d.Thermostat.Goal, 64)
 	t.Temperature = goalTimesTwo * 0.5
-	t.ain = d.Identifier
 	return t
 }
diff --git a/manifest/model.go b/manifest/model.go
index c201f4d..8d9cac2 100644
--- a/manifest/model.go
+++ b/manifest/model.go
@@ -10,14 +10,12 @@ type Plan struct {
 type Switch struct {
 	Name  string // Name of the switch.
 	State bool   // On (true) or off (false).
-	ain   string // AIN identifier.
 }
 
 // Thermostat represents the state of a HKR device.
 type Thermostat struct {
 	Name        string  // Name of the device.
 	Temperature float64 // The temperature in °C.
-	ain         string  // AIN identifier.
 }
 
 func (plan *Plan) switchNamed(name string) (sw Switch, ok bool) {


Test patch:
diff --git a/fritz/aha_concurrent_test.go b/fritz/aha_concurrent_test.go
deleted file mode 100644
index 38e50bb..0000000
--- a/fritz/aha_concurrent_test.go
+++ /dev/null
@@ -1,142 +0,0 @@
-package fritz
-
-import (
-	"fmt"
-	"net/http/httptest"
-	"net/url"
-	"reflect"
-	"runtime"
-	"testing"
-
-	"github.com/bpicode/fritzctl/mock"
-	"github.com/stretchr/testify/assert"
-)
-
-// TestConcurrentFritzAPI test the FRITZ API.
-func TestConcurrentFritzAPI(t *testing.T) {
-
-	testCases := []struct {
-		test func(t *testing.T, fritz *concurrentAhaHTTP)
-	}{
-		{testAPISwitchDeviceOn},
-		{testAPISwitchDeviceOff},
-		{testAPISwitchDeviceOffErrorUnknownDevice},
-		{testAPISwitchDeviceOnErrorUnknownDevice},
-		{testAPIToggleDevice},
-		{testAPISetHkr},
-		{testAPISetHkrDevNotFound},
-		{testToggleConcurrent},
-		{testToggleConcurrentWithOneError},
-		{testToggleConcurrentWithDeviceNotFound},
-	}
-	for _, tc := range testCases {
-		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(tc.test).Pointer()).Name()), func(t *testing.T) {
-			mockFritz := mock.New().Start()
-			defer mockFritz.Close()
-			t, ha := createHaClient(mockFritz, t)
-			tc.test(t, ha)
-		})
-	}
-}
-
-func createHaClient(mock *mock.Fritz, t *testing.T) (*testing.T, *concurrentAhaHTTP) {
-	u, err := url.Parse(mock.Server.URL)
-	assert.NoError(t, err)
-	client, err := NewClient("../mock/client_config_template.json")
-	assert.NoError(t, err)
-	client.Config.Net.Protocol = u.Scheme
-	client.Config.Net.Host = u.Host
-	err = client.Login()
-	assert.NoError(t, err)
-	ha := concurrentConfigurator(HomeAutomation(client)).(*concurrentAhaHTTP)
-	assert.NotNil(t, ha)
-	return t, ha
-}
-
-func testAPISetHkr(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.temp(12.5, "HKR_2")
-	assert.NoError(t, err)
-}
-
-func testAPISetHkrDevNotFound(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.temp(12.5, "DOES-NOT-EXIST")
-	assert.Error(t, err)
-}
-
-func testAPISetHkrErrorServerDownAtCommandStage(t *testing.T, fritz *concurrentAhaHTTP, server *httptest.Server) {
-	server.Close()
-	err := fritz.temp(12.5, "HKR_1")
-	assert.Error(t, err)
-}
-
-func testAPISwitchDeviceOn(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.on("SWITCH_1")
-	assert.NoError(t, err)
-}
-
-func testAPISwitchDeviceOff(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.off("SWITCH_2")
-	assert.NoError(t, err)
-}
-
-func testAPISwitchDeviceOffErrorUnknownDevice(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.off("DEVICE_THAT_DOES_NOT_EXIST")
-	assert.Error(t, err)
-}
-
-func testAPISwitchDeviceOnErrorUnknownDevice(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.on("DEVICE_THAT_DOES_NOT_EXIST")
-	assert.Error(t, err)
-}
-
-func testAPIToggleDevice(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.toggle("SWITCH_2")
-	assert.NoError(t, err)
-}
-
-func testToggleConcurrent(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.toggle("SWITCH_1", "SWITCH_2", "SWITCH_3")
-	assert.NoError(t, err)
-}
-
-func testToggleConcurrentWithOneError(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.toggle("SWITCH_1", "SWITCH_2", "SWITCH_3", "SWITCH_4_FAILING")
-	assert.Error(t, err)
-}
-
-func testToggleConcurrentWithDeviceNotFound(t *testing.T, fritz *concurrentAhaHTTP) {
-	err := fritz.toggle("SWITCH_1", "UNKNOWN", "SWITCH_3")
-	assert.Error(t, err)
-}
-
-// TestConcurrentFritzAPIWithServerShutDown test the FRITZ API.
-func TestConcurrentFritzAPIWithServerShutDown(t *testing.T) {
-
-	testCases := []struct {
-		test func(t *testing.T, fritz *concurrentAhaHTTP, server *httptest.Server)
-	}{
-		{testAPISwitchDeviceOffErrorServerDownAtListingStage},
-		{testAPIToggleDeviceErrorServerDownAtListingStage},
-		{testAPISetHkrErrorServerDownAtCommandStage},
-	}
-	for _, tc := range testCases {
-		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(tc.test).Pointer()).Name()), func(t *testing.T) {
-			mockFritz := mock.New().Start()
-			defer mockFritz.Close()
-			t, ha := createHaClient(mockFritz, t)
-			tc.test(t, ha, mockFritz.Server)
-		})
-	}
-}
-
-func testAPISwitchDeviceOffErrorServerDownAtListingStage(t *testing.T, fritz *concurrentAhaHTTP, server *httptest.Server) {
-	server.Close()
-	err := fritz.off("SWITCH_1")
-	assert.Error(t, err)
-}
-
-func testAPIToggleDeviceErrorServerDownAtListingStage(t *testing.T, fritz *concurrentAhaHTTP, server *httptest.Server) {
-	server.Close()
-	err := fritz.toggle("SWITCH_1")
-	assert.Error(t, err)
-}
diff --git a/fritz/aha_test.go b/fritz/ain_based_test.go
similarity index 73%
rename from fritz/aha_test.go
rename to fritz/ain_based_test.go
index 1f9fe05..be59e7f 100644
--- a/fritz/aha_test.go
+++ b/fritz/ain_based_test.go
@@ -13,9 +13,8 @@ import (
 	"github.com/stretchr/testify/assert"
 )
 
-// TestFritzAPI test the FRITZ API.
-func TestFritzAPI(t *testing.T) {
-
+// TestAinBased test the FRITZ API.
+func TestAinBased(t *testing.T) {
 	serverFactory := func() *httptest.Server {
 		return mock.New().UnstartedServer()
 	}
@@ -27,12 +26,12 @@ func TestFritzAPI(t *testing.T) {
 	}
 
 	testCases := []struct {
-		doTest func(t *testing.T, fritz *ahaHTTP, server *httptest.Server)
+		doTest func(t *testing.T, fritz *ainBasedClient, server *httptest.Server)
 	}{
-		{testGetDeviceList},
-		{testAPIGetDeviceListErrorServerDown},
-		{testAPISwitchOffByAinWithErrorServerDown},
-		{testAPIToggleDeviceErrorServerDownAtToggleStage},
+		{testListDevices},
+		{testListDevicesErrorServerDown},
+		{testSwitchForAinErrorServerDown},
+		{testToggleErrorServerDown},
 	}
 	for _, testCase := range testCases {
 		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(testCase.doTest).Pointer()).Name()), func(t *testing.T) {
@@ -46,15 +45,15 @@ func TestFritzAPI(t *testing.T) {
 			client.Config.Net.Host = u.Host
 			err = client.Login()
 			assert.NoError(t, err)
-			ha := HomeAutomation(client).(*ahaHTTP)
+			ha := newAinBased(client).(*ainBasedClient)
 			assert.NotNil(t, ha)
 			testCase.doTest(t, ha, server)
 		})
 	}
 }
 
-func testGetDeviceList(t *testing.T, fritz *ahaHTTP, server *httptest.Server) {
-	devList, err := fritz.ListDevices()
+func testListDevices(t *testing.T, fritz *ainBasedClient, _ *httptest.Server) {
+	devList, err := fritz.listDevices()
 	log.Println(*devList)
 	assert.NoError(t, err)
 	assert.NotNil(t, devList)
@@ -69,21 +68,21 @@ func testGetDeviceList(t *testing.T, fritz *ahaHTTP, server *httptest.Server) {
 
 }
 
-func testAPIGetDeviceListErrorServerDown(t *testing.T, fritz *ahaHTTP, server *httptest.Server) {
+func testListDevicesErrorServerDown(t *testing.T, fritz *ainBasedClient, server *httptest.Server) {
 	server.Close()
-	_, err := fritz.ListDevices()
+	_, err := fritz.listDevices()
 	assert.Error(t, err)
 }
 
-func testAPISwitchOffByAinWithErrorServerDown(t *testing.T, fritz *ahaHTTP, server *httptest.Server) {
+func testSwitchForAinErrorServerDown(t *testing.T, fritz *ainBasedClient, server *httptest.Server) {
 	server.Close()
 	_, err := fritz.switchForAin("123344", "off")
 	assert.Error(t, err)
 }
 
-func testAPIToggleDeviceErrorServerDownAtToggleStage(t *testing.T, fritz *ahaHTTP, server *httptest.Server) {
+func testToggleErrorServerDown(t *testing.T, fritz *ainBasedClient, server *httptest.Server) {
 	server.Close()
-	_, err := fritz.Toggle("DER device")
+	_, err := fritz.toggle("DER device")
 	assert.Error(t, err)
 }
 
@@ -113,11 +112,11 @@ func TestRounding(t *testing.T) {
 // TestUnacceptableTempValues asserts that temperatures outside the range of the spec are perceived as invalid.
 func TestUnacceptableTempValues(t *testing.T) {
 	assertions := assert.New(t)
-	h := HomeAutomation(nil)
+	h := newAinBased(nil)
 
-	_, err := h.ApplyTemperature(7.5, "1235")
+	_, err := h.applyTemperature(7.5, "1235")
 	assertions.Error(err)
 
-	_, err = h.ApplyTemperature(55, "1235")
+	_, err = h.applyTemperature(55, "1235")
 	assertions.Error(err)
 }
diff --git a/fritz/api_aha_test.go b/fritz/api_aha_test.go
new file mode 100644
index 0000000..e9eda70
--- /dev/null
+++ b/fritz/api_aha_test.go
@@ -0,0 +1,200 @@
+package fritz
+
+import (
+	"fmt"
+	"net/http/httptest"
+	"net/url"
+	"reflect"
+	"runtime"
+	"sync"
+	"testing"
+
+	"github.com/bpicode/fritzctl/mock"
+	"github.com/stretchr/testify/assert"
+)
+
+// TestApiUsage tests how the API feels.
+func TestApiUsage(t *testing.T) {
+	assertions := assert.New(t)
+	u, _ := url.Parse("https://localhost:12345")
+	h := NewHomeAuto(
+		Credentials("", "password"),
+		SkipTLSVerify(),
+		Certificate([]byte{}),
+		URL(u),
+		AuthEndpoint("/login_sid.lua"),
+	)
+	assertions.NotNil(h)
+}
+
+// TestNoPanic tests that the API functions should not panic.
+func TestNoPanic(t *testing.T) {
+	assertions := assert.New(t)
+	u, _ := url.Parse("https://localhost:12345")
+	h := NewHomeAuto(
+		Credentials("", "password"),
+		SkipTLSVerify(),
+		URL(u),
+	)
+	assertions.NotPanics(func() {
+		h.Login()
+		h.List()
+		h.Temp(20.0, "dev_name")
+		h.On("dev_name")
+		h.Off("dev_name")
+		h.Toggle("dev_name")
+	})
+
+}
+
+// TestFritzAPI test the FRITZ API.
+func TestFritzAPI(t *testing.T) {
+	testCases := []struct {
+		test func(t *testing.T, h HomeAuto)
+	}{
+		{testOn},
+		{testOff},
+		{testOffError},
+		{testOnError},
+		{testToggle},
+		{testTemp},
+		{testTempErrorDeviceNotFound},
+		{testToggleMany},
+		{testToggleError},
+		{testToggleErrorDeviceNotFound},
+	}
+	for _, tc := range testCases {
+		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(tc.test).Pointer()).Name()), func(t *testing.T) {
+			mockFritz := mock.New().Start()
+			defer mockFritz.Close()
+			ha := login(mockFritz, t)
+			tc.test(t, ha)
+		})
+	}
+}
+
+func login(mock *mock.Fritz, t *testing.T) HomeAuto {
+	u, err := url.Parse(mock.Server.URL)
+	assert.NoError(t, err)
+	client, err := NewClient("../mock/client_config_template.json")
+	assert.NoError(t, err)
+	client.Config.Net.Protocol = u.Scheme
+	client.Config.Net.Host = u.Host
+	err = client.Login()
+	assert.NoError(t, err)
+	return &homeAuto{client: client, aha: newAinBased(client)}
+}
+
+func testTemp(t *testing.T, fritz HomeAuto) {
+	err := fritz.Temp(12.5, "HKR_2")
+	assert.NoError(t, err)
+}
+
+func testTempErrorDeviceNotFound(t *testing.T, h HomeAuto) {
+	err := h.Temp(12.5, "DOES-NOT-EXIST")
+	assert.Error(t, err)
+}
+
+func testOn(t *testing.T, h HomeAuto) {
+	err := h.On("SWITCH_1")
+	assert.NoError(t, err)
+}
+
+func testOnError(t *testing.T, h HomeAuto) {
+	err := h.On("DEVICE_THAT_DOES_NOT_EXIST")
+	assert.Error(t, err)
+}
+
+func testOff(t *testing.T, h HomeAuto) {
+	err := h.Off("SWITCH_2")
+	assert.NoError(t, err)
+}
+
+func testOffError(t *testing.T, h HomeAuto) {
+	err := h.Off("DEVICE_THAT_DOES_NOT_EXIST")
+	assert.Error(t, err)
+}
+
+func testToggle(t *testing.T, h HomeAuto) {
+	err := h.Toggle("SWITCH_2")
+	assert.NoError(t, err)
+}
+
+func testToggleMany(t *testing.T, h HomeAuto) {
+	err := h.Toggle("SWITCH_1", "SWITCH_2", "SWITCH_3")
+	assert.NoError(t, err)
+}
+
+func testToggleError(t *testing.T, h HomeAuto) {
+	err := h.Toggle("SWITCH_1", "SWITCH_2", "SWITCH_3", "SWITCH_4_FAILING")
+	assert.Error(t, err)
+}
+
+func testToggleErrorDeviceNotFound(t *testing.T, fritz HomeAuto) {
+	err := fritz.Toggle("SWITCH_1", "UNKNOWN", "SWITCH_3")
+	assert.Error(t, err)
+}
+
+// TestWithServerShutDown test the FRITZ API error handling when the backend is unreachable spontaneously.
+func TestWithServerShutDown(t *testing.T) {
+	testCases := []struct {
+		test func(t *testing.T, h HomeAuto, s *httptest.Server)
+	}{
+		{testOffErrorServerDown},
+		{testToggleServerDown},
+		{testTempServerDown},
+	}
+	for _, tc := range testCases {
+		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(tc.test).Pointer()).Name()), func(t *testing.T) {
+			mockFritz := mock.New().Start()
+			defer mockFritz.Close()
+			ha := login(mockFritz, t)
+			tc.test(t, ha, mockFritz.Server)
+		})
+	}
+}
+
+func testOffErrorServerDown(t *testing.T, h HomeAuto, s *httptest.Server) {
+	s.Close()
+	err := h.Off("SWITCH_1")
+	assert.Error(t, err)
+}
+
+func testToggleServerDown(t *testing.T, h HomeAuto, s *httptest.Server) {
+	s.Close()
+	err := h.Toggle("SWITCH_1")
+	assert.Error(t, err)
+}
+
+func testTempServerDown(t *testing.T, h HomeAuto, s *httptest.Server) {
+	s.Close()
+	err := h.Temp(12.5, "HKR_1")
+	assert.Error(t, err)
+}
+
+// TestDeviceListCaching tests behavior when caching is turned on.
+func TestDeviceListCaching(t *testing.T) {
+	mockFritz := mock.New().Start()
+	defer mockFritz.Close()
+	u, err := url.Parse(mockFritz.Server.URL)
+	assert.NoError(t, err)
+	h := NewHomeAuto(
+		URL(u),
+		Caching(true),
+		SkipTLSVerify(),
+	)
+	err = h.Login()
+	assert.NoError(t, err)
+
+	var wg sync.WaitGroup
+	wg.Add(10)
+	for i := 0; i < 10; i++ {
+		go func() {
+			defer wg.Done()
+			l, err := h.List()
+			assert.NoError(t, err)
+			assert.NotNil(t, l)
+		}()
+	}
+	wg.Wait()
+}
diff --git a/fritz/internal_test.go b/fritz/api_internal_test.go
similarity index 68%
rename from fritz/internal_test.go
rename to fritz/api_internal_test.go
index 75e8876..2e40b9c 100644
--- a/fritz/internal_test.go
+++ b/fritz/api_internal_test.go
@@ -14,14 +14,14 @@ import (
 // TestInternalFritzAPI test the FRITZ API.
 func TestInternalFritzAPI(t *testing.T) {
 	testCases := []struct {
-		dotest func(t *testing.T, internal *internalHTTP)
+		tc func(t *testing.T, internal Internal)
 	}{
 		{testListLanDevices},
 		{testListLogs},
 		{testInetStats},
 	}
 	for _, testCase := range testCases {
-		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(testCase.dotest).Pointer()).Name()), func(t *testing.T) {
+		t.Run(fmt.Sprintf("Test aha api %s", runtime.FuncForPC(reflect.ValueOf(testCase.tc).Pointer()).Name()), func(t *testing.T) {
 			srv := mock.New().Start()
 			defer srv.Close()
 
@@ -35,27 +35,27 @@ func TestInternalFritzAPI(t *testing.T) {
 			err = client.Login()
 			assert.NoError(t, err)
 
-			internal := Internal(client).(*internalHTTP)
+			internal := NewInternal(client)
 			assert.NotNil(t, internal)
-			testCase.dotest(t, internal)
+			testCase.tc(t, internal)
 		})
 	}
 }
 
-func testInetStats(t *testing.T, internal *internalHTTP) {
-	_, err := internal.InternetStats()
+func testInetStats(t *testing.T, i Internal) {
+	_, err := i.InternetStats()
 	assert.NoError(t, err)
 }
 
-func testListLanDevices(t *testing.T, internal *internalHTTP) {
-	list, err := internal.ListLanDevices()
+func testListLanDevices(t *testing.T, i Internal) {
+	list, err := i.ListLanDevices()
 	assert.NoError(t, err)
 	assert.NotNil(t, list)
 	assert.Len(t, list.Network, 3)
 }
 
-func testListLogs(t *testing.T, internal *internalHTTP) {
-	list, err := internal.ListLogs()
+func testListLogs(t *testing.T, i Internal) {
+	list, err := i.ListLogs()
 	assert.NoError(t, err)
 	assert.NotNil(t, list)
 	assert.Len(t, list.Messages, 7)
diff --git a/fritz/api_test.go b/fritz/api_test.go
deleted file mode 100644
index cf30b1e..0000000
--- a/fritz/api_test.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package fritz
-
-import (
-	"net/url"
-	"testing"
-
-	"github.com/stretchr/testify/assert"
-)
-
-// TestApiUsage tests how the API feels.
-func TestApiUsage(t *testing.T) {
-	assertions := assert.New(t)
-	u, _ := url.Parse("https://localhost:12345")
-	h := NewHomeAuto(
-		Credentials("", "password"),
-		SkipTLSVerify(),
-		Certificate([]byte{}),
-		URL(u),
-		AuthEndpoint("/login_sid.lua"),
-	)
-	assertions.NotNil(h)
-}
-
-// TestNoPanic tests that the API functions should not panic.
-func TestNoPanic(t *testing.T) {
-	assertions := assert.New(t)
-	u, _ := url.Parse("https://localhost:12345")
-	h := NewHomeAuto(
-		Credentials("", "password"),
-		SkipTLSVerify(),
-		URL(u),
-	)
-	assertions.NotPanics(func() {
-		h.Login()
-		h.List()
-		h.Temp(20.0, "dev_name")
-		h.On("dev_name")
-		h.Off("dev_name")
-		h.Toggle("dev_name")
-	})
-
-}
diff --git a/logger/example_test.go b/logger/example_test.go
new file mode 100644
index 0000000..820b554
--- /dev/null
+++ b/logger/example_test.go
@@ -0,0 +1,47 @@
+package logger_test
+
+import (
+	"flag"
+
+	"github.com/bpicode/fritzctl/logger"
+)
+
+// Logging can be turned off manually.
+func ExampleLevel_Set() {
+	l := &logger.Level{}
+	l.Set("none")
+	logger.Warn("log statement")
+	// Output:
+}
+
+// Level can be used with the flag package.
+func ExampleLevel() {
+	l := logger.Level{}
+	flag.Var(&l, "log", "logging verbosity, e.g. 'info'")
+	flag.Parse()
+}
+
+// Log on "info" level.
+func ExampleInfo() {
+	logger.Info("informational message")
+}
+
+// Log on "warn" level.
+func ExampleWarn() {
+	logger.Warn("a warning")
+}
+
+// Log on "error" level.
+func ExampleError() {
+	logger.Error("an error occurred")
+}
+
+// Log on "debug" level.
+func ExampleDebug() {
+	logger.Debug("debug message")
+}
+
+// Log on "success" level.
+func ExampleSuccess() {
+	logger.Success("successfully reached a milestone in my program flow")
+}
diff --git a/logger/flags_test.go b/logger/flags_test.go
index bf54b36..a047459 100644
--- a/logger/flags_test.go
+++ b/logger/flags_test.go
@@ -10,7 +10,7 @@ import (
 // TestRegisterLoglevel is a unit test for setting the loglevel via CLI flag.
 func TestRegisterLoglevel(t *testing.T) {
 	f := flag.NewFlagSet("TestFlagSet", flag.PanicOnError)
-	ll := Loglevel{}
+	ll := Level{}
 	f.Var(&ll, "loglevel", "logging verbosity specified as "+ll.Type())
 	err := f.Parse([]string{"--loglevel=info"})
 	assert.NoError(t, err)
@@ -19,7 +19,7 @@ func TestRegisterLoglevel(t *testing.T) {
 // TestRegisterLoglevel is a unit test for setting the loglevel via CLI flag.
 func TestRegisterLoglevelInvalid(t *testing.T) {
 	f := flag.NewFlagSet("TestFlagSet", flag.ContinueOnError)
-	ll := Loglevel{}
+	ll := Level{}
 	f.Var(&ll, "loglevel", "logging verbosity specified as "+ll.Type())
 	err := f.Parse([]string{"--loglevel=whistleblower"})
 	assert.Error(t, err)
diff --git a/manifest/applier_aha_http_test.go b/manifest/applier_aha_http_test.go
index d23d46e..17429e9 100644
--- a/manifest/applier_aha_http_test.go
+++ b/manifest/applier_aha_http_test.go
@@ -2,7 +2,6 @@ package manifest
 
 import (
 	"errors"
-	"strconv"
 	"testing"
 
 	"github.com/stretchr/testify/assert"
@@ -11,24 +10,24 @@ import (
 type fritzAlwaysSuccess struct {
 }
 
-// SwitchOn always succeeds.
-func (f *fritzAlwaysSuccess) SwitchOn(ain string) (string, error) {
-	return "1", nil
+// On always succeeds.
+func (f *fritzAlwaysSuccess) On(names ...string) error {
+	return nil
 }
 
-// SwitchOff always succeeds.
-func (f *fritzAlwaysSuccess) SwitchOff(ain string) (string, error) {
-	return "0", nil
+// Off always succeeds.
+func (f *fritzAlwaysSuccess) Off(names ...string) error {
+	return nil
 }
 
-// ApplyTemperature always succeeds.
-func (f *fritzAlwaysSuccess) ApplyTemperature(value float64, ain string) (string, error) {
-	return strconv.FormatFloat(value*2.0, 'f', -1, 64), nil
+// Temp always succeeds.
+func (f *fritzAlwaysSuccess) Temp(value float64, names ...string) error {
+	return nil
 }
 
 // TestApplyViaAha tests the http interface applier.
 func TestApplyViaAha(t *testing.T) {
-	applier := AhaAPIApplier(&fritzAlwaysSuccess{})
+	applier := NewApplier(&fritzAlwaysSuccess{})
 	err := applier.Apply(
 		&Plan{
 			Switches:    []Switch{{Name: "s", State: true}},
@@ -43,7 +42,7 @@ func TestApplyViaAha(t *testing.T) {
 
 // TestApplyViaAhaLargeSystem tests the http interface applier.
 func TestApplyViaAhaLargeSystem(t *testing.T) {
-	applier := AhaAPIApplier(&fritzAlwaysSuccess{})
+	applier := NewApplier(&fritzAlwaysSuccess{})
 	err := applier.Apply(
 		&Plan{
 			Switches: []Switch{
@@ -107,24 +106,24 @@ func TestApplyViaAhaLargeSystem(t *testing.T) {
 type fritzAlwaysError struct {
 }
 
-// SwitchOn always returns an error.
-func (f *fritzAlwaysError) SwitchOn(ain string) (string, error) {
-	return "", errors.New("That didn't work")
+// On always returns an error.
+func (f *fritzAlwaysError) On(names ...string) error {
+	return errors.New("that didn't work")
 }
 
-// SwitchOff always returns an error.
-func (f *fritzAlwaysError) SwitchOff(ain string) (string, error) {
-	return "", errors.New("That didn't work")
+// Off always returns an error.
+func (f *fritzAlwaysError) Off(names ...string) error {
+	return errors.New("that didn't work")
 }
 
-// ApplyTemperature always returns an error.
-func (f *fritzAlwaysError) ApplyTemperature(value float64, ain string) (string, error) {
-	return "", errors.New("That didn't work")
+// Temp always returns an error.
+func (f *fritzAlwaysError) Temp(value float64, names ...string) error {
+	return errors.New("that didn't work")
 }
 
 // TestApplyViaAhaErrorByThermostat tests the http interface applier.
 func TestApplyViaAhaErrorByThermostat(t *testing.T) {
-	applier := AhaAPIApplier(&fritzAlwaysError{})
+	applier := NewApplier(&fritzAlwaysError{})
 	err := applier.Apply(
 		&Plan{
 			Switches:    []Switch{{Name: "s", State: true}},
@@ -139,7 +138,7 @@ func TestApplyViaAhaErrorByThermostat(t *testing.T) {
 
 // TestApplyViaAhaErrorBySwitch tests the http interface applier.
 func TestApplyViaAhaErrorBySwitch(t *testing.T) {
-	applier := AhaAPIApplier(&fritzAlwaysError{})
+	applier := NewApplier(&fritzAlwaysError{})
 	err := applier.Apply(
 		&Plan{
 			Switches:    []Switch{{Name: "s", State: false}},
@@ -154,7 +153,7 @@ func TestApplyViaAhaErrorBySwitch(t *testing.T) {
 
 // TestApplyViaAhaErrorBySwitch tests the http interface applier.
 func TestApplyViaAhaErrorByMalformedPlan(t *testing.T) {
-	applier := AhaAPIApplier(&fritzAlwaysError{})
+	applier := NewApplier(&fritzAlwaysError{})
 	err := applier.Apply(
 		&Plan{
 			Switches:    []Switch{{Name: "s", State: false}},
@@ -169,7 +168,7 @@ func TestApplyViaAhaErrorByMalformedPlan(t *testing.T) {
 
 // TestApplyViaAhaLargeSystemWithErrors tests the http interface applier.
 func TestApplyViaAhaLargeSystemWithErrors(t *testing.T) {
-	applier := AhaAPIApplier(&fritzAlwaysError{})
+	applier := NewApplier(&fritzAlwaysError{})
 	err := applier.Apply(
 		&Plan{
 			Switches: []Switch{